Files
studiobeve.no/application/models/Service_model.php
T
Astral04andClaude Sonnet 4.6 b8f4f67b23 Add visual worker calendar and floating lunch break system
- Monthly calendar grid per worker with colour-coded day status
- Override types: vacation, sick, custom hours, other, day-off
- Date-range override support via modal
- Floating 30-min lunch break: finds slot closest to preferred time
  within configurable window, adapts to existing bookings
- Lunch break only applies for shifts >= 6 hours
- Lunch slot shown in admin calendar; blocked in booking availability
- DB migrations: absence_type/note on worker_schedule_overrides,
  lunch_window_start/end/preferred_time on workers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 13:34:58 +01:00

920 lines
32 KiB
PHP
Executable File

<?php
class Service_model extends CI_Model {
function __construct(){
parent::__construct();
}
public function createService($serviceArray){
$this->load->database();
$query = $this->db->query('INSERT INTO services (
service_type,
service_category_no,
service_category_en,
service_category_hu,
service_name_no,
service_name_en,
service_name_hu,
service_description_no,
service_description_en,
service_description_hu,
service_price,
service_time,
service_category_id,
is_enabled
) VALUES(
"'.$serviceArray['service_type'].'",
"'.$serviceArray['service_category_no'].'",
"'.$serviceArray['service_category_en'].'",
"'.$serviceArray['service_category_hu'].'",
"'.$serviceArray['service_name_no'].'",
"'.$serviceArray['service_name_en'].'",
"'.$serviceArray['service_name_hu'].'",
"'.$serviceArray['service_description_no'].'",
"'.$serviceArray['service_description_en'].'",
"'.$serviceArray['service_description_hu'].'",
"'.$serviceArray['service_price'].'",
"'.$serviceArray['service_time'].'",
"'.$serviceArray['service_category_id'].'",
"'.$serviceArray['is_enabled'].'"
);');
}
public function createServiceCategory($serviceCategoryArray){
$this->load->database();
$query = $this->db->query('INSERT INTO service_categories (
serv_cat_slug,
serv_cat_name
) VALUES(
"'.$serviceCategoryArray['serv_cat_slug'].'",
"'.$serviceCategoryArray['serv_cat_name'].'"
);');
}
public function updateService($service_id, $serviceArray){
$this->load->database();
foreach($serviceArray as $propertyKey => $propertyValue){
$query = $this->db->query('UPDATE services SET '.$propertyKey.' = "'.$propertyValue.'" WHERE service_id = "'.$service_id.'";');
}
}
public function updateServiceCategory($service_category_id, $serviceCategoryArray){
$this->load->database();
foreach($serviceCategoryArray as $propertyKey => $propertyValue){
$query = $this->db->query('UPDATE service_categories SET '.$propertyKey.' = "'.$propertyValue.'" WHERE service_category_id = "'.$service_category_id.'";');
}
}
public function createWorker($workerArray){
$this->load->database();
$query = $this->db->query('INSERT INTO workers (
worker_name,
worker_profile_img,
worker_info,
is_beauty,
is_barber,
service_category_id
) VALUES(
"'.$workerArray['worker_name'].'",
"'.$workerArray['worker_profile_img'].'",
"'.$workerArray['worker_info'].'",
"'.$workerArray['is_beauty'].'",
"'.$workerArray['is_barber'].'",
"'.$workerArray['service_category_id'].'"
);');
}
public function updateWorker($worker_id, $workerArray){
$this->load->database();
foreach($workerArray as $propertyKey => $propertyValue){
$query = $this->db->query('UPDATE workers SET '.$propertyKey.' = "'.$propertyValue.'" WHERE worker_id = "'.$worker_id.'";');
}
}
public function updateBooking($booking_id, $bookingArray){
$this->load->database();
$this->db->where('booking_id', $booking_id);
$this->db->update('bookings', $bookingArray);
}
public function getAllServiceByServiceType($service_type, $lang){
$this->load->database();
$query = $this->db->query('SELECT * FROM services JOIN service_categories ON services.service_category_id = service_categories.service_category_id WHERE service_type = "'.$service_type.'" AND services.is_enabled = "1" AND services.is_deleted != "1" ORDER BY service_id ASC;');
$serviceArray = array();
if($query->num_rows() > 0){
foreach($query->result() as $resultItem){
$service_category = '';
$service_name = '';
$service_description = '';
switch($lang){
case 'no':
$service_category = $resultItem->service_category_no;
$service_name = $resultItem->service_name_no;
$service_description = $resultItem->service_description_no;
break;
case 'en':
$service_category = $resultItem->service_category_en;
$service_name = $resultItem->service_name_en;
$service_description = $resultItem->service_description_en;
break;
case 'hu':
$service_category = $resultItem->service_category_hu;
$service_name = $resultItem->service_name_hu;
$service_description = $resultItem->service_description_hu;
break;
}
$resultItemTmp = array(
'service_id' => $resultItem->service_id,
'service_type' => $resultItem->service_type,
'service_category' => $service_category,
'service_name' => $service_name,
'service_description' => $service_description,
'service_price' => $resultItem->service_price,
'service_time' => $resultItem->service_time,
'serv_cat_slug' => $resultItem->serv_cat_slug,
'service_category_id' => $resultItem->service_category_id
);
$serviceArray[] = (object)$resultItemTmp;
}
}
return $serviceArray;
}
public function getServiceById($service_id){
$this->load->database();
$query = $this->db->query('SELECT * FROM services WHERE service_id = "'.$service_id.'" LIMIT 1;');
if($query->num_rows() > 0){
return $query->result()[0];
}
else{
return 0;
}
}
public function getServiceCategoryById($service_category_id){
$this->load->database();
$query = $this->db->query('SELECT * FROM service_categories WHERE service_category_id = "'.$service_category_id.'" LIMIT 1;');
if($query->num_rows() > 0){
return $query->result()[0];
}
else{
return 0;
}
}
public function getAllServices(){
$this->load->database();
$query = $this->db->query('SELECT * FROM services JOIN service_categories ON services.service_category_id = service_categories.service_category_id WHERE services.is_deleted != "1" ORDER BY service_type ASC, service_name_hu ASC;');
if($query->num_rows() > 0){
return $query->result();
}
else{
return 0;
}
}
public function getAllServiceCategories(){
$this->load->database();
$query = $this->db->query('SELECT * FROM service_categories WHERE is_deleted != "1" ORDER BY service_category_id;');
if($query->num_rows() > 0){
return $query->result();
}
else{
return 0;
}
}
public function getAvailableServices(){
$this->load->database();
$query = $this->db->query('SELECT * FROM services WHERE is_enabled = "1" AND is_deleted != "1" ORDER BY service_name_hu ASC;');
if($query->num_rows() > 0){
$serviceArray = array();
foreach($query->result() as $resultItem){
$serviceTmp = array(
'service_name' => $resultItem->service_name_hu.' ('.$resultItem->service_type.')',
'service_id' => $resultItem->service_id
);
$serviceArray[] = $serviceTmp;
}
return $serviceArray;
}
}
public function getAllWorkers(){
$this->load->database();
$query = $this->db->query('SELECT * FROM workers JOIN service_categories ON workers.service_category_id = service_categories.service_category_id WHERE workers.is_deleted != "1" ORDER BY worker_name ASC;');
return $query->result();
}
public function getWorkersByCategorySlug($serv_cat_slug){
$this->load->database();
$query = $this->db->query('SELECT * FROM workers JOIN service_categories ON workers.service_category_id = service_categories.service_category_id WHERE workers.is_deleted != "1" AND serv_cat_slug = "'.$serv_cat_slug.'" ORDER BY worker_name ASC;');
return $query->result();
}
public function getActiveWorkers($subpage = ''){
$this->load->database();
$subpageString = '';
if($subpage == 'beauty'){
$subpageString = ' AND is_beauty = "1"';
}
else if($subpage == 'barber'){
$subpageString = ' AND is_barber = "1"';
}
else{
$subpageString = '';
}
$query = $this->db->query('SELECT * FROM workers WHERE is_active = "1" '.$subpageString.' AND is_deleted != "1" ORDER BY worker_name ASC;');
return $query->result();
}
public function deleteBooking($booking_id){
$this->load->database();
$query = $this->db->query('DELETE FROM bookings WHERE booking_id = "'.$booking_id.'";');
}
public function getAllBookings(){
$this->load->database();
$this->load->model('Service_model');
$query = $this->db->query('SELECT * FROM bookings JOIN workers ON bookings.worker_id = workers.worker_id ORDER BY booking_date DESC, booking_start_time ASC;');
if($query->num_rows() > 0){
$resultsArray = array();
foreach($query->result() as $resultItem){
$resultTmp = (array)$resultItem;
if($resultTmp['service_ids'] != ''){
$servicesArray = unserialize($resultTmp['service_ids']);
if(is_array($servicesArray)){
$services = array();
foreach($servicesArray as $servicesArrayItem){
$selectedService = $this->Service_model->getServiceById($servicesArrayItem);
$services[] = (object)array(
'service_type' => $selectedService->service_type,
'service_name' => $selectedService->service_name_hu,
'service_price' => $selectedService->service_price,
'service_time' => $selectedService->service_time
);
}
$resultTmp['services'] = $services;
}
else{
$resultTmp['services'] = array();
}
}
else{
$resultTmp['services'] = array();
}
$resultsArray[] = (object)$resultTmp;
}
return $resultsArray;
}
else{
return 0;
}
}
public function getActualBookings(){
$this->load->database();
$this->load->model('Service_model');
$query = $this->db->query('SELECT * FROM bookings JOIN workers ON bookings.worker_id = workers.worker_id WHERE booking_date >= CURDATE() ORDER BY booking_date DESC, booking_start_time ASC;');
if($query->num_rows() > 0){
$resultsArray = array();
foreach($query->result() as $resultItem){
$resultTmp = (array)$resultItem;
if($resultTmp['service_ids'] != ''){
$servicesArray = unserialize($resultTmp['service_ids']);
if(is_array($servicesArray)){
$services = array();
foreach($servicesArray as $servicesArrayItem){
$selectedService = $this->Service_model->getServiceById($servicesArrayItem);
$services[] = (object)array(
'service_type' => $selectedService->service_type,
'service_name' => $selectedService->service_name_hu,
'service_price' => $selectedService->service_price,
'service_time' => $selectedService->service_time
);
}
$resultTmp['services'] = $services;
}
else{
$resultTmp['services'] = array();
}
}
else{
$resultTmp['services'] = array();
}
$resultsArray[] = (object)$resultTmp;
}
return $resultsArray;
}
else{
return 0;
}
}
public function getActualBookingsByWorkerID($workerID){
$this->load->database();
$this->load->model('Service_model');
$query = $this->db->query('SELECT * FROM bookings JOIN workers ON bookings.worker_id = workers.worker_id WHERE booking_date >= CURDATE() AND bookings.worker_id = "'.$workerID.'" ORDER BY booking_date DESC, booking_start_time ASC;');
if($query->num_rows() > 0){
$resultsArray = array();
foreach($query->result() as $resultItem){
$resultTmp = (array)$resultItem;
if($resultTmp['service_ids'] != ''){
$servicesArray = unserialize($resultTmp['service_ids']);
if(is_array($servicesArray)){
$services = array();
foreach($servicesArray as $servicesArrayItem){
$selectedService = $this->Service_model->getServiceById($servicesArrayItem);
$services[] = (object)array(
'service_type' => $selectedService->service_type,
'service_name' => $selectedService->service_name_hu,
'service_price' => $selectedService->service_price,
'service_time' => $selectedService->service_time
);
}
$resultTmp['services'] = $services;
}
else{
$resultTmp['services'] = array();
}
}
else{
$resultTmp['services'] = array();
}
$resultsArray[] = (object)$resultTmp;
}
return $resultsArray;
}
else{
return 0;
}
}
public function getBookingById($booking_id){
$this->load->database();
$this->load->model('Service_model');
$query = $this->db->query('SELECT * FROM bookings WHERE booking_id = "'.$booking_id.'" LIMIT 1;');
if($query->num_rows() > 0){
return $query->result()[0];
}
else{
return 0;
}
}
public function getWorkerById($worker_id){
$this->load->database();
$query = $this->db->query('SELECT * FROM workers WHERE worker_id = "'.$worker_id.'" LIMIT 1;');
if($query->num_rows() > 0){
return $query->result()[0];
}
else{
return 0;
}
}
public function getAvailableTimes($worker_id, $selectedDate, $servicelength) {
$this->load->database();
// múltbeli napra ne lehessen foglalni
if (strtotime($selectedDate) < strtotime(date('Y-m-d'))) {
return [];
}
// max 3 hónappal előre
$today = new DateTime();
$maxDate = (clone $today)->modify('+3 months');
$selected = new DateTime($selectedDate);
if ($selected > $maxDate) {
return [];
}
$ts = strtotime($selectedDate);
$debugDate = date('Y-m-d (w)', $ts) . ' - ' . date('l', $ts);
log_message('error', "📅 DEBUG DATE CHECK for '{$selectedDate}': {$debugDate}");
log_message('error', "⏱ Timezone: " . date_default_timezone_get());
$serviceLengthTime = strtotime($servicelength);
$availableTimeArray = [];
// --- Override ellenőrzés ---
$overrideQuery = $this->db->query("
SELECT * FROM worker_schedule_overrides
WHERE worker_id = '$worker_id' AND date = '$selectedDate'
LIMIT 1
");
if ($overrideQuery->num_rows() > 0) {
$override = $overrideQuery->row();
if ($override->is_day_off) return [];
$dayStartTime = new DateTime($selectedDate.' '.$override->start_time);
$finishTime = new DateTime($selectedDate.' '.$override->end_time);
} else {
$weekday = date('w', strtotime($selectedDate)); // 0 = Sunday, 6 = Saturday
$weekNumber = date('W', strtotime($selectedDate));
$isOddWeek = $weekNumber % 2 !== 0;
$scheduleQuery = $this->db->query("
SELECT * FROM worker_schedule
WHERE worker_id = '{$worker_id}'
AND weekday = '{$weekday}'
AND (
is_alternate_week = 0
OR (is_alternate_week = 1 AND " . ($isOddWeek ? "1" : "0") . ")
)
");
if ($scheduleQuery->num_rows() == 0) {
return [];
}
$schedule = $scheduleQuery->row();
$dayStartTime = new DateTime($selectedDate . ' ' . $schedule->start_time);
$finishTime = new DateTime($selectedDate . ' ' . $schedule->end_time);
}
// --- ÚJ: ma foglalva csak (következő 15 perces blokk + 1 óra) UTÁN legyen időpont ---
if ($selectedDate == date('Y-m-d')) {
// használd a helyi (Oslo) időt, ne az esetleges UTC defaultot
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
// kerekítés a következő 15 perces blokkra
$minutes = (int)$now->format('i');
$mod = $minutes % 15;
if ($mod !== 0) {
$now->modify('+' . (15 - $mod) . ' minutes');
}
// erre dobunk +1 órát
$now->modify('+1 hour');
$now->setTime((int)$now->format('H'), (int)$now->format('i'), 0);
// ha ez később van, mint a munkanap kezdete, toljuk fel a nap kezdetét
if ($now > $dayStartTime) {
$dayStartTime = clone $now;
}
log_message('error', '⏳ Min start (next 15min +1h, Europe/Oslo): ' . $dayStartTime->format('Y-m-d H:i:s'));
}
// --- Lunch break: find free 30-min slot closest to preferred time within worker's lunch window ---
$lunchBlockStart = null;
$lunchBlockEnd = null;
$workerRow = $this->db->query("SELECT lunch_window_start, lunch_window_end, lunch_preferred_time FROM workers WHERE worker_id = '$worker_id' LIMIT 1")->row();
$shiftSeconds = $finishTime->getTimestamp() - $dayStartTime->getTimestamp();
if ($workerRow && $workerRow->lunch_window_start && $workerRow->lunch_window_end && $shiftSeconds >= 6 * 3600) {
$allBookings = $this->db->query("
SELECT booking_start_time, booking_finish_time FROM bookings
WHERE booking_date = '$selectedDate' AND worker_id = '$worker_id'
")->result();
$lunchWinStart = new DateTime($selectedDate . ' ' . $workerRow->lunch_window_start);
$lunchWinEnd = new DateTime($selectedDate . ' ' . $workerRow->lunch_window_end);
$lunchDuration = new DateInterval('PT30M');
$lunchStep = new DateInterval('PT15M');
// Clamp window to actual shift hours
if ($lunchWinStart < $dayStartTime) $lunchWinStart = clone $dayStartTime;
if ($lunchWinEnd > $finishTime) $lunchWinEnd = clone $finishTime;
$scanEnd = (clone $lunchWinEnd)->sub($lunchDuration);
$isFree = function($c) use ($lunchDuration, $allBookings, $selectedDate) {
$cEnd = (clone $c)->add($lunchDuration);
foreach ($allBookings as $bk) {
$bkS = new DateTime($selectedDate . ' ' . $bk->booking_start_time);
$bkE = new DateTime($selectedDate . ' ' . $bk->booking_finish_time);
if ($bkS < $cEnd && $bkE > $c) return false;
}
return true;
};
// Collect free slots inside window, pick closest to preferred time
$freeSlots = [];
for ($c = clone $lunchWinStart; $c <= $scanEnd; $c->add($lunchStep)) {
if ($isFree($c)) $freeSlots[] = clone $c;
}
if (!empty($freeSlots)) {
$prefDt = $workerRow->lunch_preferred_time
? new DateTime($selectedDate . ' ' . $workerRow->lunch_preferred_time)
: clone $lunchWinStart;
$best = null;
$bestDiff = PHP_INT_MAX;
foreach ($freeSlots as $slot) {
$diff = abs($slot->getTimestamp() - $prefDt->getTimestamp());
if ($diff < $bestDiff) { $bestDiff = $diff; $best = $slot; }
}
$lunchBlockStart = $best;
$lunchBlockEnd = (clone $best)->add($lunchDuration);
} else {
// Fallback: search after the window
for ($c = clone $lunchWinEnd; ; $c->add($lunchStep)) {
$candEnd = (clone $c)->add($lunchDuration);
if ($candEnd > $finishTime) break;
if ($isFree($c)) { $lunchBlockStart = clone $c; $lunchBlockEnd = $candEnd; break; }
}
}
}
$interval = new DateInterval('PT15M');
list($h, $m) = explode(':', $servicelength);
$serviceDuration = new DateInterval('PT' . (int)$h . 'H' . (int)$m . 'M');
for ($current = clone $dayStartTime; $current < $finishTime; $current->add($interval)) {
$slotEnd = clone $current;
$slotEnd->add($serviceDuration);
if ($slotEnd > $finishTime) break;
$startTimeStr = $current->format('H:i:s');
$endTimeStr = $slotEnd->format('H:i:s');
$bookingQuery = $this->db->query("
SELECT * FROM bookings
WHERE booking_date = '{$selectedDate}'
AND worker_id = '{$worker_id}'
AND (
(booking_start_time < '{$endTimeStr}' AND booking_finish_time > '{$startTimeStr}')
)
");
$overlapsLunch = $lunchBlockStart && ($current < $lunchBlockEnd) && ($slotEnd > $lunchBlockStart);
if ($bookingQuery->num_rows() == 0 && !$overlapsLunch) {
$availableTimeArray[] = $startTimeStr;
}
log_message('debug', 'Selected Date: ' . $selectedDate);
log_message('debug', 'Weekday: ' . (isset($weekday) ? $weekday : 'N/A'));
log_message('debug', 'Worker ID: ' . $worker_id);
log_message('debug', 'Query: ' . $this->db->last_query());
}
return $availableTimeArray;
}
public function createBooking($bookingArray){
$this->load->database();
$query = $this->db->query("INSERT INTO bookings (
guest_name,
guest_email,
guest_phone,
worker_id,
booking_date,
booking_start_time,
booking_finish_time,
service_ids,
guest_confirmed,
guest_confirm_code,
manage_token
) VALUES(
'".$bookingArray['guest_name']."',
'".$bookingArray['guest_email']."',
'".$bookingArray['guest_phone']."',
'".$bookingArray['worker_id']."',
'".$bookingArray['booking_date']."',
'".$bookingArray['booking_start_time']."',
'".$bookingArray['booking_finish_time']."',
'".$bookingArray['service_ids']."',
'".$bookingArray['guest_confirmed']."',
'".$bookingArray['guest_confirm_code']."',
'".$bookingArray['manage_token']."'
);");
}
public function getBookingByToken($token){
$this->load->database();
$query = $this->db->query('SELECT * FROM bookings WHERE manage_token = "'.$this->db->escape_str($token).'" LIMIT 1;');
return $query->num_rows() > 0 ? $query->result()[0] : false;
}
public function updateBookingCalEvents($booking_id, $worker_event_id, $owner_event_id){
$this->load->database();
$this->db->query("UPDATE bookings SET gcal_event_id_worker = '".$this->db->escape_str($worker_event_id)."', gcal_event_id_owner = '".$this->db->escape_str($owner_event_id)."' WHERE booking_id = '".$this->db->escape_str($booking_id)."';");
}
public function getWorkerSchedule($worker_id) {
return $this->db
->where('worker_id', $worker_id)
->order_by('weekday', 'ASC')
->get('worker_schedule')
->result();
}
public function clearWorkerSchedule($worker_id) {
$this->load->database();
$this->db->where('worker_id', $worker_id)->delete('worker_schedule');
}
public function addWorkerSchedule($data) {
$this->db->insert('worker_schedule', $data);
}
public function getAllWorkerSchedules() {
$query = $this->db->query('SELECT * FROM worker_schedule');
return $query->result();
}
public function getAllWorkerOverrides() {
$query = $this->db->query('SELECT * FROM worker_schedule_overrides');
return $query->result();
}
public function getWorkerScheduleOverrides() {
$this->load->database();
$query = $this->db->query("
SELECT o.*, w.worker_name
FROM worker_schedule_overrides o
JOIN workers w ON o.worker_id = w.worker_id
ORDER BY o.date DESC
");
return $query->result();
}
public function createWorkerScheduleOverride($override) {
$this->db->insert('worker_schedule_overrides', $override);
}
public function getBookingsForWorkerMonth($worker_id, $year, $month) {
$this->load->database();
$yearMonth = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
$query = $this->db->query("
SELECT booking_date, booking_start_time, booking_finish_time FROM bookings
WHERE worker_id = ? AND DATE_FORMAT(booking_date,'%Y-%m') = ?
ORDER BY booking_date, booking_start_time
", [$worker_id, $yearMonth]);
$result = [];
foreach ($query->result() as $row) {
$result[$row->booking_date][] = $row;
}
return $result;
}
public function computeLunchBreak($winStart, $winEnd, $date, $dayBookings, $shiftEnd, $shiftStart = null, $preferredTime = null) {
// Lunch break only applies for shifts of 6+ hours
if ($shiftStart && ($shiftEnd->getTimestamp() - $shiftStart->getTimestamp()) < 6 * 3600) {
return null;
}
$lunchDuration = new DateInterval('PT30M');
$lunchStep = new DateInterval('PT15M');
$winStartDt = new DateTime($date . ' ' . $winStart);
$winEndDt = new DateTime($date . ' ' . $winEnd);
// Clamp window to actual shift hours
if ($shiftStart && $winStartDt < $shiftStart) $winStartDt = clone $shiftStart;
if ($winEndDt > $shiftEnd) $winEndDt = clone $shiftEnd;
$scanEnd = (clone $winEndDt)->sub($lunchDuration);
$isFree = function($candidate) use ($lunchDuration, $dayBookings, $date) {
$candEnd = (clone $candidate)->add($lunchDuration);
foreach ($dayBookings as $bk) {
$bkS = new DateTime($date . ' ' . $bk->booking_start_time);
$bkE = new DateTime($date . ' ' . $bk->booking_finish_time);
if ($bkS < $candEnd && $bkE > $candidate) return false;
}
return true;
};
// Collect all free candidate slots within window
$freeSlots = [];
for ($c = clone $winStartDt; $c <= $scanEnd; $c->add($lunchStep)) {
if ($isFree($c)) $freeSlots[] = clone $c;
}
if (!empty($freeSlots)) {
// Pick the slot closest to preferred time (or start of window if no preference)
$prefDt = $preferredTime
? new DateTime($date . ' ' . $preferredTime)
: clone $winStartDt;
$best = null;
$bestDiff = PHP_INT_MAX;
foreach ($freeSlots as $slot) {
$diff = abs($slot->getTimestamp() - $prefDt->getTimestamp());
if ($diff < $bestDiff) { $bestDiff = $diff; $best = $slot; }
}
$bestEnd = (clone $best)->add($lunchDuration);
return ['start' => $best->format('H:i'), 'end' => $bestEnd->format('H:i')];
}
// Fallback: search after the window
for ($c = clone $winEndDt; ; $c->add($lunchStep)) {
$candEnd = (clone $c)->add($lunchDuration);
if ($candEnd > $shiftEnd) break;
if ($isFree($c)) return ['start' => $c->format('H:i'), 'end' => $candEnd->format('H:i')];
}
return null;
}
public function getWorkerOverridesByMonth($worker_id, $year, $month) {
$this->load->database();
$yearMonth = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
$query = $this->db->query(
"SELECT * FROM worker_schedule_overrides WHERE worker_id = ? AND DATE_FORMAT(date,'%Y-%m') = ?",
[$worker_id, $yearMonth]
);
$result = [];
foreach ($query->result() as $row) {
$result[$row->date] = $row;
}
return $result;
}
public function getWorkerScheduleOverrideByDate($worker_id, $date) {
$this->load->database();
$this->db->where('worker_id', $worker_id);
$this->db->where('date', $date);
$this->db->limit(1);
return $this->db->get('worker_schedule_overrides')->row();
}
public function upsertWorkerScheduleOverride($worker_id, $date, $data) {
$existing = $this->getWorkerScheduleOverrideByDate($worker_id, $date);
if ($existing) {
$this->db->where('worker_id', $worker_id);
$this->db->where('date', $date);
$this->db->update('worker_schedule_overrides', $data);
} else {
$data['worker_id'] = $worker_id;
$data['date'] = $date;
$this->db->insert('worker_schedule_overrides', $data);
}
}
public function deleteWorkerScheduleOverrideByDate($worker_id, $date) {
$this->load->database();
$this->db->where('worker_id', $worker_id);
$this->db->where('date', $date);
$this->db->delete('worker_schedule_overrides');
}
public function getWorkerScheduleByDay($worker_id, $weekday) {
$this->db->where('worker_id', $worker_id);
$this->db->where('weekday', $weekday);
$query = $this->db->get('worker_schedule');
return $query->row(); // null if no row
}
public function isEvenWeek($date) {
return ((int)date('W', strtotime($date)) % 2) === 0;
}
public function isWorkerAvailableThisWeek($worker_id, $selectedDate) {
$weekday = date('w', strtotime($selectedDate));
$weekNumber = date('W', strtotime($selectedDate));
$isOddWeek = $weekNumber % 2 !== 0;
$query = $this->db->query("
SELECT * FROM worker_schedule
WHERE worker_id = '{$worker_id}'
AND weekday = '{$weekday}'
AND (
is_alternate_week = 0
OR (is_alternate_week = 1 AND " . ($isOddWeek ? "1" : "0") . ")
)
");
return $query->num_rows() > 0;
}
}
?>