diff --git a/application/config/routes.php b/application/config/routes.php index 443dcab..0203361 100755 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -89,6 +89,10 @@ $route['workers'] = 'admin/workers'; $route['workers/add-worker'] = 'admin/add_worker'; $route['workers/update-worker/(:any)'] = 'admin/update_worker/$1'; $route['workers/worker-process'] = 'admin/worker_process'; +$route['workers/worker-calendar'] = 'admin/worker_calendar'; +$route['workers/worker-calendar/(:any)'] = 'admin/worker_calendar/$1'; +$route['workers/store-calendar-override'] = 'admin/store_calendar_override'; +$route['workers/delete-calendar-override'] = 'admin/delete_calendar_override'; $route['bookings'] = 'admin/bookings'; $route['bookings/add-booking'] = 'admin/add_booking'; diff --git a/application/controllers/Admin.php b/application/controllers/Admin.php index a47019a..15a7e1a 100755 --- a/application/controllers/Admin.php +++ b/application/controllers/Admin.php @@ -651,7 +651,10 @@ class Admin extends CI_Controller { 'service_category_id' => $_POST['service_category_id'], 'is_active' => $_POST['is_active'], 'google_calendar_id' => isset($_POST['google_calendar_id']) ? $_POST['google_calendar_id'] : '', - 'ntfy_topic' => isset($_POST['ntfy_topic']) ? $_POST['ntfy_topic'] : '' + 'ntfy_topic' => isset($_POST['ntfy_topic']) ? $_POST['ntfy_topic'] : '', + 'lunch_window_start' => !empty($_POST['lunch_window_start']) ? $_POST['lunch_window_start'] : null, + 'lunch_window_end' => !empty($_POST['lunch_window_end']) ? $_POST['lunch_window_end'] : null, + 'lunch_preferred_time' => !empty($_POST['lunch_preferred_time']) ? $_POST['lunch_preferred_time'] : null, ); $this->Service_model->updateWorker($_POST['worker_id'], $workerArray); $this->Log_model->addLog('workers', $data['currentUser']->username.' módosította a(z) '.$_POST['worker_name'].' nevű dolgozót ('.implode(',',$workerArray).')', $data['currentUser']->username); @@ -798,6 +801,24 @@ class Admin extends CI_Controller { if(isset($_GET['delete-booking'])){ $selectedBooking = $this->Service_model->getBookingById($_GET['delete-booking']); + // Google Calendar: delete events before removing booking + $this->load->library('GoogleCalendar'); + $this->config->load('google_calendar'); + $delWorker = $this->Service_model->getWorkerById($selectedBooking->worker_id); + if(!empty($selectedBooking->gcal_event_id_worker) && $delWorker && !empty($delWorker->google_calendar_id)){ + $this->googlecalendar->deleteEvent($delWorker->google_calendar_id, $selectedBooking->gcal_event_id_worker); + } + $evelinCalIdDel = $this->config->item('gcal_evelin_calendar_id'); + if(!empty($selectedBooking->gcal_event_id_owner) && !empty($evelinCalIdDel)){ + $this->googlecalendar->deleteEvent($evelinCalIdDel, $selectedBooking->gcal_event_id_owner); + } + // ntfy push notification + $this->_ntfy( + 'Admin deleted booking: ' . $selectedBooking->guest_name, + 'Worker: ' . ($delWorker ? $delWorker->worker_name : '?') . "\n" . + 'Date: ' . $selectedBooking->booking_date . ' ' . $selectedBooking->booking_start_time, + ($delWorker && isset($delWorker->ntfy_topic)) ? $delWorker->ntfy_topic : null + ); $this->Service_model->deleteBooking($_GET['delete-booking']); $this->Log_model->addLog('bookings', $data['currentUser']->username.' törölte a(z) '.$_GET['delete-booking'].' számú foglalást ('.$selectedBooking->guest_name.', '.$selectedBooking->guest_email.', '.$selectedBooking->guest_phone.', '.$selectedBooking->booking_date.', '.$selectedBooking->booking_start_time.', '.$selectedBooking->booking_finish_time.', '.$selectedBooking->service_ids.')', $data['currentUser']->username); header('Location:'.SITEURL.'bookings'); @@ -852,7 +873,7 @@ class Admin extends CI_Controller { $this->Log_model->addLog('bookings', $data['currentUser']->username.' létrehoza a(z) '.$_POST['guest_name'].' nevű vendég foglalást ('.implode(', ', $bookingArray).')', $data['currentUser']->username); $this->Service_model->createBooking($bookingArray); } - + if($_POST['send_notification']){ $worker = $this->Service_model->getWorkerById($bookingArray['worker_id']); @@ -961,10 +982,6 @@ class Admin extends CI_Controller { } if(isset($_POST['updateBookingManual'])){ - $booking_date = new DateTime(date('Y-m-d', strtotime($_POST['booking_date']))); - $end_booking_date = new DateTime(date('Y-m-d', strtotime($_POST['end_booking_date']))); - $diff = $booking_date->diff($end_booking_date); - $selectedServiceArray = array(); if($_POST['service_ids'] != ''){ @@ -972,45 +989,66 @@ class Admin extends CI_Controller { } - if($_POST['booking_date'] != $_POST['end_booking_date'] && $booking_date <= $end_booking_date){ - for($index = 0; $index <= $diff->days; $index++){ - $selectedDate = date('Y-m-d', strtotime($_POST['booking_date']. ' + '.$index.' days')); - - - $bookingArray = array( - 'guest_name' => $_POST['guest_name'], - 'guest_email' => $_POST['guest_email'], - 'guest_phone' => $_POST['guest_phone'], - 'worker_id' => $_POST['worker_id'], - 'booking_date' => $selectedDate, - 'booking_start_time' => $_POST['booking_start_time'], - 'booking_finish_time' => $_POST['booking_finish_time'], - 'service_ids' => serialize($selectedServiceArray), - 'guest_confirmed' => '0', - 'guest_confirm_code' => '1234' - ); - - $this->Service_model->createBooking($bookingArray); + $bookingArray = array( + 'guest_name' => $_POST['guest_name'], + 'guest_email' => $_POST['guest_email'], + 'guest_phone' => $_POST['guest_phone'], + 'worker_id' => $_POST['worker_id'], + 'booking_date' => $_POST['booking_date'], + 'booking_start_time' => $_POST['booking_start_time'], + 'booking_finish_time' => $_POST['booking_finish_time'], + 'service_ids' => serialize($selectedServiceArray), + 'guest_confirmed' => '0', + 'guest_confirm_code' => '1234' + ); + $oldBooking = $this->Service_model->getBookingById($_POST['booking_id']); + $this->Service_model->updateBooking($_POST['booking_id'], $bookingArray); + $this->Log_model->addLog('bookings', $data['currentUser']->username.' módosította a foglalását a(z) '.$bookingArray['guest_name'].' nevű vendégnek ('.implode(', ', $bookingArray).')', $data['currentUser']->username); + // Google Calendar: delete old events, create new ones + $this->load->library('GoogleCalendar'); + $this->config->load('google_calendar'); + $evelinCalId = $this->config->item('gcal_evelin_calendar_id'); + if($oldBooking){ + $oldWorker = $this->Service_model->getWorkerById($oldBooking->worker_id); + if(!empty($oldBooking->gcal_event_id_worker) && $oldWorker && !empty($oldWorker->google_calendar_id)){ + $this->googlecalendar->deleteEvent($oldWorker->google_calendar_id, $oldBooking->gcal_event_id_worker); + } + if(!empty($oldBooking->gcal_event_id_owner) && !empty($evelinCalId)){ + $this->googlecalendar->deleteEvent($evelinCalId, $oldBooking->gcal_event_id_owner); } } - else{ - $bookingArray = array( - 'guest_name' => $_POST['guest_name'], - 'guest_email' => $_POST['guest_email'], - 'guest_phone' => $_POST['guest_phone'], - 'worker_id' => $_POST['worker_id'], - 'booking_date' => $_POST['booking_date'], - 'booking_start_time' => $_POST['booking_start_time'], - 'booking_finish_time' => $_POST['booking_finish_time'], - 'service_ids' => serialize($selectedServiceArray), - 'guest_confirmed' => '0', - 'guest_confirm_code' => '1234' - ); - - $this->Service_model->updateBooking($_POST['booking_id'], $bookingArray); - $this->Log_model->addLog('bookings', $data['currentUser']->username.' módosította a foglalását a(z) '.$bookingArray['guest_name'].' nevű vendégnek ('.implode(', ', $bookingArray).')', $data['currentUser']->username); + $gcalWorker = $this->Service_model->getWorkerById($bookingArray['worker_id']); + $gcalServices = unserialize($bookingArray['service_ids']); + $gcalServiceArray = array(); + if(is_array($gcalServices)){ + foreach($gcalServices as $sid){ + $svc = $this->Service_model->getServiceById($sid); + if(is_object($svc)) $gcalServiceArray[] = $svc; + } } - + $gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $gcalServiceArray)); + $gcalTitle = $gcalWorker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $bookingArray['guest_name']; + $gcalDescription = 'Guest: ' . $bookingArray['guest_name'] . "\nPhone: " . $bookingArray['guest_phone'] . "\nEmail: " . $bookingArray['guest_email']; + $gcalStart = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_start_time']; + $gcalEnd = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_finish_time']; + $newWorkerEventId = null; + $newOwnerEventId = null; + if(!empty($gcalWorker->google_calendar_id)){ + $newWorkerEventId = $this->googlecalendar->createEvent($gcalWorker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd); + } + if(!empty($evelinCalId)){ + $newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd); + } + $this->Service_model->updateBookingCalEvents($_POST['booking_id'], $newWorkerEventId, $newOwnerEventId); + // ntfy push notification + $this->_ntfy( + 'Admin modified booking: ' . $bookingArray['guest_name'], + 'Worker: ' . $gcalWorker->worker_name . "\n" . + 'Date: ' . $bookingArray['booking_date'] . ' ' . $bookingArray['booking_start_time'] . ' - ' . $bookingArray['booking_finish_time'] . "\n" . + 'Services: ' . $gcalServiceNames, + isset($gcalWorker->ntfy_topic) ? $gcalWorker->ntfy_topic : null + ); + if($_POST['send_notification']){ $worker = $this->Service_model->getWorkerById($bookingArray['worker_id']); @@ -1126,6 +1164,26 @@ class Admin extends CI_Controller { } } + private function _ntfy($title, $message, $workerTopic = null){ + $this->config->load('google_calendar'); + $evelinTopic = $this->config->item('ntfy_topic'); + $topics = []; + if(!empty($evelinTopic)) $topics[] = $evelinTopic; + if(!empty($workerTopic) && $workerTopic !== $evelinTopic) $topics[] = $workerTopic; + if(empty($topics)) return; + foreach($topics as $topic){ + $ch = curl_init('https://ntfy.sh/' . $topic); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $message, + CURLOPT_HTTPHEADER => ['Title: ' . $title, 'Priority: high', 'Tags: calendar'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + ]); + curl_exec($ch); + curl_close($ch); + } + } @@ -1143,9 +1201,157 @@ class Admin extends CI_Controller { + public function worker_calendar($worker_id = null) { + $this->load->helper('url'); + $this->load->model('Service_model'); + $this->load->model('User_model'); + $this->load->model('Module_model'); + if (!isset($_SESSION['username'])) { + header("Location:".SITEURL."login"); + exit; + } + $data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']); + if ($this->User_model->is_user_admin($_SESSION['username'])) { + $data['activeModules'] = $this->Module_model->getAllModule(); + } else { + $data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id); + } + $data['workers'] = $this->Service_model->getAllWorkers(); + $data['selectedWorker'] = null; + $data['overridesByDate'] = []; + $data['scheduleRows'] = []; + + $monthParam = $this->input->get('month'); + if ($monthParam && preg_match('/^\d{4}-\d{2}$/', $monthParam)) { + list($year, $month) = explode('-', $monthParam); + $year = (int)$year; + $month = (int)$month; + } else { + $year = (int)date('Y'); + $month = (int)date('m'); + } + $data['year'] = $year; + $data['month'] = $month; + + if ($worker_id) { + $data['selectedWorker'] = $this->Service_model->getWorkerById($worker_id); + $data['overridesByDate'] = $this->Service_model->getWorkerOverridesByMonth($worker_id, $year, $month); + $data['scheduleRows'] = $this->Service_model->getWorkerSchedule($worker_id); + } + + // Compute lunch break slot for every working day of the month + $data['lunchByDate'] = []; + $sw = $data['selectedWorker']; + if ($sw && !empty($sw->lunch_window_start) && !empty($sw->lunch_window_end)) { + $monthBookings = $this->Service_model->getBookingsForWorkerMonth($worker_id, $year, $month); + $scheduleMap = []; + foreach ($data['scheduleRows'] as $row) { + $scheduleMap[$row->weekday] = $row; + } + $daysInMonth = (int)date('t', mktime(0, 0, 0, $month, 1, $year)); + for ($d = 1; $d <= $daysInMonth; $d++) { + $dateStr = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-' . str_pad($d, 2, '0', STR_PAD_LEFT); + $override = isset($data['overridesByDate'][$dateStr]) ? $data['overridesByDate'][$dateStr] : null; + if ($override && $override->is_day_off) continue; + if ($override && !$override->is_day_off && $override->end_time) { + $shiftStart = new DateTime($dateStr . ' ' . $override->start_time); + $shiftEnd = new DateTime($dateStr . ' ' . $override->end_time); + } else { + $phpWeekday = (int)date('w', strtotime($dateStr)); + if (!isset($scheduleMap[$phpWeekday])) continue; + $shiftStart = new DateTime($dateStr . ' ' . $scheduleMap[$phpWeekday]->start_time); + $shiftEnd = new DateTime($dateStr . ' ' . $scheduleMap[$phpWeekday]->end_time); + } + $dayBookings = isset($monthBookings[$dateStr]) ? $monthBookings[$dateStr] : []; + $lunch = $this->Service_model->computeLunchBreak( + $sw->lunch_window_start, $sw->lunch_window_end, + $dateStr, $dayBookings, $shiftEnd, $shiftStart, + isset($sw->lunch_preferred_time) ? $sw->lunch_preferred_time : null + ); + if ($lunch) $data['lunchByDate'][$dateStr] = $lunch; + } + } + + $data['pageTitle'] = 'Munkaidő naptár'; + $this->load->view('admin/worker_calendar', $data); + } + + public function store_calendar_override() { + $this->load->model('Service_model'); + $this->load->helper('url'); + + if (!isset($_SESSION['username'])) { + header("Location:".SITEURL."login"); + exit; + } + + $worker_id = $this->input->post('worker_id'); + $start_date = $this->input->post('start_date'); + $end_date = $this->input->post('end_date') ?: $start_date; + $type = $this->input->post('override_type'); + $start_time = $this->input->post('start_time'); + $end_time = $this->input->post('end_time'); + $note = $this->input->post('note'); + + $current = strtotime($start_date); + $last = strtotime($end_date); + $guard = 0; + + while ($current <= $last && $guard < 366) { + $dateStr = date('Y-m-d', $current); + if ($type === 'remove') { + $this->Service_model->deleteWorkerScheduleOverrideByDate($worker_id, $dateStr); + } else { + $data = []; + if ($type === 'day_off') { + $data['is_day_off'] = 1; + $data['absence_type'] = null; + $data['start_time'] = null; + $data['end_time'] = null; + $data['note'] = null; + } elseif ($type === 'custom') { + $data['is_day_off'] = 0; + $data['absence_type'] = 'custom'; + $data['start_time'] = $start_time; + $data['end_time'] = $end_time; + $data['note'] = $note; + } else { + // vacation, sick, other + $data['is_day_off'] = 1; + $data['absence_type'] = $type; + $data['start_time'] = null; + $data['end_time'] = null; + $data['note'] = $note; + } + $this->Service_model->upsertWorkerScheduleOverride($worker_id, $dateStr, $data); + } + $current = strtotime('+1 day', $current); + $guard++; + } + + $monthParam = date('Y-m', strtotime($start_date)); + redirect('workers/worker-calendar/' . $worker_id . '?month=' . $monthParam); + } + + public function delete_calendar_override() { + $this->load->model('Service_model'); + $this->load->helper('url'); + + if (!isset($_SESSION['username'])) { + header("Location:".SITEURL."login"); + exit; + } + + $worker_id = $this->input->post('worker_id'); + $date = $this->input->post('date'); + + $this->Service_model->deleteWorkerScheduleOverrideByDate($worker_id, $date); + $monthParam = date('Y-m', strtotime($date)); + redirect('workers/worker-calendar/' . $worker_id . '?month=' . $monthParam); + } } diff --git a/application/models/Service_model.php b/application/models/Service_model.php index a31667d..0b2d277 100755 --- a/application/models/Service_model.php +++ b/application/models/Service_model.php @@ -96,10 +96,8 @@ class Service_model extends CI_Model { public function updateBooking($booking_id, $bookingArray){ $this->load->database(); - - foreach($bookingArray as $propertyKey => $propertyValue){ - $query = $this->db->query("UPDATE bookings SET ".$propertyKey." = '".$propertyValue."' WHERE booking_id = '".$booking_id."';"); - } + $this->db->where('booking_id', $booking_id); + $this->db->update('bookings', $bookingArray); } public function getAllServiceByServiceType($service_type, $lang){ @@ -490,6 +488,67 @@ class Service_model extends CI_Model { 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'); @@ -511,7 +570,8 @@ class Service_model extends CI_Model { ) "); - if ($bookingQuery->num_rows() == 0) { + $overlapsLunch = $lunchBlockStart && ($current < $lunchBlockEnd) && ($slotEnd > $lunchBlockStart); + if ($bookingQuery->num_rows() == 0 && !$overlapsLunch) { $availableTimeArray[] = $startTimeStr; } @@ -608,6 +668,120 @@ class Service_model extends CI_Model { 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); diff --git a/application/views/admin/includes/update-worker-form.php b/application/views/admin/includes/update-worker-form.php index 01967bb..a2cf42b 100755 --- a/application/views/admin/includes/update-worker-form.php +++ b/application/views/admin/includes/update-worker-form.php @@ -52,6 +52,18 @@ +
+ + +
+
+ + +
+
+ + +
diff --git a/application/views/admin/includes/worker-calendar-form.php b/application/views/admin/includes/worker-calendar-form.php new file mode 100644 index 0000000..5183cd2 --- /dev/null +++ b/application/views/admin/includes/worker-calendar-form.php @@ -0,0 +1,322 @@ + + +
+
+ + + +
+ + +
+ + row + $scheduleMap = []; + foreach ($scheduleRows as $row) { + $scheduleMap[$row->weekday] = $row; + } + + // Calendar math + $firstDay = mktime(0, 0, 0, $month, 1, $year); + $daysInMonth = (int)date('t', $firstDay); + // day-of-week of 1st (0=Sun, so Mon=1) + $startDow = (int)date('w', $firstDay); // 0=Sun..6=Sat + // Convert to Mon-based: Mon=0 .. Sun=6 + $startOffset = ($startDow + 6) % 7; + + $today = date('Y-m-d'); + $monthLabel = strftime('%Y %B', $firstDay); // Hungarian month names via locale + // Fallback: PHP intl not guaranteed; build own table + $monthNames = ['január','február','március','április','május','június','július','augusztus','szeptember','október','november','december']; + $monthLabel = $year . ' ' . $monthNames[$month - 1]; + + $prevMonth = $month == 1 ? 12 : $month - 1; + $prevYear = $month == 1 ? $year - 1 : $year; + $nextMonth = $month == 12 ? 1 : $month + 1; + $nextYear = $month == 12 ? $year + 1 : $year; + $prevParam = $prevYear . '-' . str_pad($prevMonth, 2, '0', STR_PAD_LEFT); + $nextParam = $nextYear . '-' . str_pad($nextMonth, 2, '0', STR_PAD_LEFT); + $workerId = $selectedWorker->worker_id; +?> + + + + + +
+
Munkanap
+
Szabad nap
+
Napiszabadság
+
Szabadság
+
Betegszabadság
+
Egyedi munkaidő
+
Egyéb
+
Ebédszünet (auto)
+
+ + + + + + + PHP weekday=1, ... Sun=6 -> PHP=0 + echo ''; + // Empty cells before first day + for ($i = 0; $i < $startOffset; $i++) { + echo ''; + $cell++; + } + while ($day <= $daysInMonth) { + if ($cell % 7 === 0 && $cell > 0) { + echo ''; + } + $colIndex = $cell % 7; // 0=Mon .. 6=Sun + $phpWeekday = $phpDow[$colIndex]; // PHP weekday number + + $dateStr = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-' . str_pad($day, 2, '0', STR_PAD_LEFT); + $isToday = ($dateStr === $today); + + // Determine status + $override = isset($overridesByDate[$dateStr]) ? $overridesByDate[$dateStr] : null; + $hasSchedule = isset($scheduleMap[$phpWeekday]); + + $badgeHtml = ''; + $cellTitle = ''; + $overrideJson = 'null'; + + if ($override) { + $absType = $override->absence_type ?? null; + if ($absType === 'vacation') { + $badgeHtml = 'Szabadság'; + $cellTitle = 'Szabadság'; + } elseif ($absType === 'sick') { + $badgeHtml = 'Beteg'; + $cellTitle = 'Betegszabadság'; + } elseif ($absType === 'custom') { + $st = substr($override->start_time, 0, 5); + $et = substr($override->end_time, 0, 5); + $badgeHtml = ''.$st.'–'.$et.''; + $cellTitle = 'Egyedi: '.$st.'–'.$et; + } elseif ($absType === 'other') { + $badgeHtml = 'Egyéb'; + $cellTitle = 'Egyéb'; + } else { + // is_day_off=1, no absence_type = plain day off + $badgeHtml = 'Szabad'; + $cellTitle = 'Napi szabadság'; + } + $overrideJson = json_encode([ + 'is_day_off' => (int)$override->is_day_off, + 'absence_type' => $override->absence_type, + 'start_time' => $override->start_time ? substr($override->start_time, 0, 5) : '', + 'end_time' => $override->end_time ? substr($override->end_time, 0, 5) : '', + 'note' => $override->note ?? '', + ]); + } elseif ($hasSchedule) { + $sch = $scheduleMap[$phpWeekday]; + $st = substr($sch->start_time, 0, 5); + $et = substr($sch->end_time, 0, 5); + $badgeHtml = ''.$st.'–'.$et.''; + $cellTitle = 'Munkanap: '.$st.'–'.$et; + } else { + $badgeHtml = 'Szabad'; + $cellTitle = 'Nem dolgozik'; + } + + $todayClass = $isToday ? ' cal-today' : ''; + $encodedDate = htmlspecialchars(json_encode($dateStr), ENT_QUOTES); + $encodedWid = htmlspecialchars(json_encode($workerId), ENT_QUOTES); + $encodedOver = htmlspecialchars($overrideJson, ENT_QUOTES); + echo ''; + + $day++; + $cell++; + } + // Pad remaining cells + while ($cell % 7 !== 0) { + echo ''; + $cell++; + } + echo ''; + ?> +
HKSzCsPSzV
'; + echo '
'.$day.'
'; + echo $badgeHtml; + if (isset($lunchByDate[$dateStr])) { + $lb = $lunchByDate[$dateStr]; + echo '🍴 '.$lb['start'].'–'.$lb['end'].''; + } + echo '
+ + +
+ + +
+
+

Nap szerkesztése

+
+ + + + + + + + + + +
+ +
+ + +
+ + +
+
+
+
+ + diff --git a/application/views/admin/includes/workers-form.php b/application/views/admin/includes/workers-form.php index 7eda0ad..3f79b2f 100755 --- a/application/views/admin/includes/workers-form.php +++ b/application/views/admin/includes/workers-form.php @@ -23,6 +23,7 @@ + | | #worker_id;?> diff --git a/application/views/admin/worker_calendar.php b/application/views/admin/worker_calendar.php new file mode 100644 index 0000000..e0ff169 --- /dev/null +++ b/application/views/admin/worker_calendar.php @@ -0,0 +1,5 @@ +