Add visual booking calendar (weekly Teams-style view)
- New weekly calendar at /bookings/calendar with time blocks per booking - Color-coded by worker, overlapping bookings shown side-by-side - Click booking to see details, edit or delete - Worker filter dropdown, week navigation (prev/next/today) - AJAX week loading for smooth navigation - Link between list view and calendar view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a100acddec
commit
6abb90329d
@@ -95,6 +95,8 @@ $route['workers/store-calendar-override'] = 'admin/store_calendar_override';
|
|||||||
$route['workers/delete-calendar-override'] = 'admin/delete_calendar_override';
|
$route['workers/delete-calendar-override'] = 'admin/delete_calendar_override';
|
||||||
|
|
||||||
$route['bookings'] = 'admin/bookings';
|
$route['bookings'] = 'admin/bookings';
|
||||||
|
$route['bookings/calendar'] = 'admin/booking_calendar';
|
||||||
|
$route['bookings/calendar-data'] = 'admin/booking_calendar_data';
|
||||||
$route['bookings/add-booking'] = 'admin/add_booking';
|
$route['bookings/add-booking'] = 'admin/add_booking';
|
||||||
$route['bookings/update-booking/(:any)'] = 'admin/update_booking/$1';
|
$route['bookings/update-booking/(:any)'] = 'admin/update_booking/$1';
|
||||||
$route['bookings/booking-process'] = 'admin/booking_process';
|
$route['bookings/booking-process'] = 'admin/booking_process';
|
||||||
|
|||||||
@@ -695,6 +695,96 @@ class Admin extends CI_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function booking_calendar(){
|
||||||
|
$this->load->helper('url');
|
||||||
|
$this->load->model('User_model');
|
||||||
|
$this->load->model('Service_model');
|
||||||
|
$this->load->model('Module_model');
|
||||||
|
|
||||||
|
if(isset($_SESSION['username'])){
|
||||||
|
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
|
||||||
|
$data['pageTitle'] = 'Foglalási naptár';
|
||||||
|
$data['message'] = '';
|
||||||
|
$data['message_class'] = '';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute week bounds
|
||||||
|
$weekParam = isset($_GET['week']) ? $_GET['week'] : date('Y-m-d');
|
||||||
|
$monday = new DateTime($weekParam);
|
||||||
|
$dow = (int)$monday->format('N'); // 1=Mon..7=Sun
|
||||||
|
if ($dow > 1) $monday->modify('-'.($dow - 1).' days');
|
||||||
|
$sunday = clone $monday;
|
||||||
|
$sunday->modify('+6 days');
|
||||||
|
|
||||||
|
$data['weekStart'] = $monday->format('Y-m-d');
|
||||||
|
$data['weekEnd'] = $sunday->format('Y-m-d');
|
||||||
|
$data['workers'] = $this->Service_model->getActiveWorkers();
|
||||||
|
|
||||||
|
$workerId = isset($_GET['worker_id']) ? $_GET['worker_id'] : null;
|
||||||
|
$data['bookings'] = $this->Service_model->getBookingsByDateRange($data['weekStart'], $data['weekEnd'], $workerId);
|
||||||
|
|
||||||
|
$this->load->view('admin/booking-calendar', $data);
|
||||||
|
} else {
|
||||||
|
header("Location:".SITEURL."login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function booking_calendar_data(){
|
||||||
|
$this->load->helper('url');
|
||||||
|
$this->load->model('User_model');
|
||||||
|
$this->load->model('Service_model');
|
||||||
|
|
||||||
|
if(!isset($_SESSION['username'])){
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(array('error' => 'unauthorized'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$weekParam = isset($_GET['week']) ? $_GET['week'] : date('Y-m-d');
|
||||||
|
$monday = new DateTime($weekParam);
|
||||||
|
$dow = (int)$monday->format('N');
|
||||||
|
if ($dow > 1) $monday->modify('-'.($dow - 1).' days');
|
||||||
|
$sunday = clone $monday;
|
||||||
|
$sunday->modify('+6 days');
|
||||||
|
|
||||||
|
$workerId = isset($_GET['worker_id']) && $_GET['worker_id'] !== '' ? $_GET['worker_id'] : null;
|
||||||
|
$bookings = $this->Service_model->getBookingsByDateRange($monday->format('Y-m-d'), $sunday->format('Y-m-d'), $workerId);
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($bookings as $b) {
|
||||||
|
$services = array();
|
||||||
|
if (is_array($b->services)) {
|
||||||
|
foreach ($b->services as $s) {
|
||||||
|
$services[] = array(
|
||||||
|
'service_name' => $s->service_name,
|
||||||
|
'service_price' => $s->service_price,
|
||||||
|
'service_time' => $s->service_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result[] = array(
|
||||||
|
'booking_id' => $b->booking_id,
|
||||||
|
'guest_name' => $b->guest_name,
|
||||||
|
'guest_email' => $b->guest_email,
|
||||||
|
'guest_phone' => $b->guest_phone,
|
||||||
|
'worker_id' => $b->worker_id,
|
||||||
|
'worker_name' => $b->worker_name,
|
||||||
|
'booking_date' => $b->booking_date,
|
||||||
|
'booking_start_time' => $b->booking_start_time,
|
||||||
|
'booking_finish_time' => $b->booking_finish_time,
|
||||||
|
'services' => $services,
|
||||||
|
'service_names_joined' => $b->service_names_joined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($result);
|
||||||
|
}
|
||||||
|
|
||||||
public function add_booking(){
|
public function add_booking(){
|
||||||
$this->load->helper('url');
|
$this->load->helper('url');
|
||||||
$this->load->model('User_model');
|
$this->load->model('User_model');
|
||||||
|
|||||||
@@ -795,6 +795,56 @@ class Service_model extends CI_Model {
|
|||||||
return $query->num_rows() > 0;
|
return $query->num_rows() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBookingsByDateRange($startDate, $endDate, $workerId = null) {
|
||||||
|
$this->load->database();
|
||||||
|
|
||||||
|
$sql = 'SELECT b.*, w.worker_name, w.worker_profile_img
|
||||||
|
FROM bookings b
|
||||||
|
JOIN workers w ON b.worker_id = w.worker_id
|
||||||
|
WHERE b.booking_date BETWEEN ? AND ?';
|
||||||
|
$params = array($startDate, $endDate);
|
||||||
|
|
||||||
|
if ($workerId) {
|
||||||
|
$sql .= ' AND b.worker_id = ?';
|
||||||
|
$params[] = $workerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' ORDER BY b.booking_date, b.booking_start_time';
|
||||||
|
$query = $this->db->query($sql, $params);
|
||||||
|
|
||||||
|
if ($query->num_rows() == 0) return array();
|
||||||
|
|
||||||
|
$resultsArray = array();
|
||||||
|
foreach ($query->result() as $row) {
|
||||||
|
$resultTmp = (array)$row;
|
||||||
|
$resultTmp['services'] = array();
|
||||||
|
$resultTmp['service_names_joined'] = '';
|
||||||
|
|
||||||
|
if (!empty($resultTmp['service_ids'])) {
|
||||||
|
$servicesArr = unserialize($resultTmp['service_ids']);
|
||||||
|
if (is_array($servicesArr)) {
|
||||||
|
$names = array();
|
||||||
|
foreach ($servicesArr as $sid) {
|
||||||
|
$svc = $this->getServiceById($sid);
|
||||||
|
if (is_object($svc)) {
|
||||||
|
$resultTmp['services'][] = (object)array(
|
||||||
|
'service_name' => $svc->service_name_hu,
|
||||||
|
'service_price' => $svc->service_price,
|
||||||
|
'service_time' => $svc->service_time
|
||||||
|
);
|
||||||
|
$names[] = $svc->service_name_hu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$resultTmp['service_names_joined'] = implode(', ', $names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultsArray[] = (object)$resultTmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resultsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
include(getcwd().'/application/views/includes/skeleton-top.php');
|
||||||
|
include(getcwd().'/application/views/admin/includes/booking-calendar-form.php');
|
||||||
|
include(getcwd().'/application/views/includes/skeleton-bottom.php');
|
||||||
|
?>
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
<?php
|
||||||
|
$colorPalette = ['#3498db','#e74c3c','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e67e22','#e91e63','#00bcd4','#8bc34a'];
|
||||||
|
$workerColorMap = array();
|
||||||
|
$i = 0;
|
||||||
|
foreach ($workers as $w) {
|
||||||
|
$workerColorMap[(string)$w->worker_id] = $colorPalette[$i % count($colorPalette)];
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build initial bookings JSON
|
||||||
|
$bookingsJson = array();
|
||||||
|
foreach ($bookings as $b) {
|
||||||
|
$services = array();
|
||||||
|
if (is_array($b->services)) {
|
||||||
|
foreach ($b->services as $s) {
|
||||||
|
$services[] = array(
|
||||||
|
'service_name' => $s->service_name,
|
||||||
|
'service_price' => $s->service_price,
|
||||||
|
'service_time' => $s->service_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bookingsJson[] = array(
|
||||||
|
'booking_id' => $b->booking_id,
|
||||||
|
'guest_name' => $b->guest_name,
|
||||||
|
'guest_email' => $b->guest_email,
|
||||||
|
'guest_phone' => $b->guest_phone,
|
||||||
|
'worker_id' => $b->worker_id,
|
||||||
|
'worker_name' => $b->worker_name,
|
||||||
|
'booking_date' => $b->booking_date,
|
||||||
|
'booking_start_time' => $b->booking_start_time,
|
||||||
|
'booking_finish_time' => $b->booking_finish_time,
|
||||||
|
'services' => $services,
|
||||||
|
'service_names_joined' => $b->service_names_joined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bcal-wrap { color: #fff; max-width: 1200px; margin: 0 auto; padding: 10px; }
|
||||||
|
.bcal-header { display: flex; align-items: center; justify-content: space-between; margin: 10px 0; }
|
||||||
|
.bcal-header a { color: #ccc; text-decoration: none; font-size: 14px; padding: 4px 10px; border: 1px solid #555; border-radius: 3px; cursor: pointer; }
|
||||||
|
.bcal-header a:hover { background: #333; }
|
||||||
|
.bcal-title { font-size: 18px; font-weight: bold; }
|
||||||
|
.bcal-toolbar { display: flex; align-items: center; gap: 12px; margin: 14px 0 6px; flex-wrap: wrap; }
|
||||||
|
.bcal-toolbar select { background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 5px 8px; border-radius: 3px; font-size: 13px; }
|
||||||
|
.bcal-toolbar select option { background: #2a2a2a; color: #fff; }
|
||||||
|
.bcal-toolbar a { color: #ccc; text-decoration: none; font-size: 13px; padding: 4px 10px; border: 1px solid #555; border-radius: 3px; }
|
||||||
|
.bcal-toolbar a:hover, .bcal-toolbar a.active { background: #444; color: #fff; }
|
||||||
|
.bcal-legend { display: flex; flex-wrap: wrap; gap: 10px; margin: 8px 0; font-size: 12px; }
|
||||||
|
.bcal-legend-item { display: flex; align-items: center; gap: 5px; }
|
||||||
|
.bcal-legend-dot { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
|
||||||
|
|
||||||
|
/* Grid */
|
||||||
|
.bcal-grid { display: grid; grid-template-columns: 55px repeat(7, 1fr); border: 1px solid #444; border-radius: 4px; overflow: hidden; }
|
||||||
|
.bcal-day-header { background: #222; text-align: center; padding: 8px 2px; font-size: 12px; font-weight: bold; border-left: 1px solid #444; border-bottom: 1px solid #444; }
|
||||||
|
.bcal-day-header.today { background: #333; color: #5cb85c; }
|
||||||
|
.bcal-time-header { background: #222; border-bottom: 1px solid #444; padding: 8px 2px; font-size: 12px; text-align: center; }
|
||||||
|
.bcal-time-col { background: #1a1a1a; }
|
||||||
|
.bcal-time-label { height: 60px; padding: 0 4px; font-size: 11px; color: #888; text-align: right; border-bottom: 1px solid #2a2a2a; display: flex; align-items: flex-start; justify-content: flex-end; padding-top: 2px; }
|
||||||
|
.bcal-day-col { position: relative; border-left: 1px solid #333; min-height: 0; }
|
||||||
|
.bcal-hour-line { position: absolute; left: 0; right: 0; border-top: 1px solid #333; pointer-events: none; z-index: 0; }
|
||||||
|
.bcal-half-line { position: absolute; left: 0; right: 0; border-top: 1px dashed #252525; pointer-events: none; z-index: 0; }
|
||||||
|
|
||||||
|
/* Booking blocks */
|
||||||
|
.bcal-booking { position: absolute; left: 1px; right: 1px; border-radius: 3px; padding: 2px 4px; font-size: 10px; line-height: 1.3; overflow: hidden; cursor: pointer; z-index: 2; color: #fff; box-sizing: border-box; border: 1px solid rgba(255,255,255,0.15); transition: filter 0.15s; }
|
||||||
|
.bcal-booking:hover { filter: brightness(1.2); z-index: 3; }
|
||||||
|
.bcal-booking .bcal-b-name { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.bcal-booking .bcal-b-svc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.85; }
|
||||||
|
.bcal-booking .bcal-b-worker { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.7; font-size: 9px; }
|
||||||
|
.bcal-booking .bcal-b-time { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.7; font-size: 9px; }
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
#bcalModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.65); z-index: 9999; }
|
||||||
|
#bcalModalBox { background: #1e1e1e; border: 1px solid #555; border-radius: 6px; max-width: 440px; margin: 80px auto; padding: 24px; color: #fff; position: relative; }
|
||||||
|
#bcalModalBox h3 { margin: 0 0 14px; font-size: 17px; }
|
||||||
|
.bcal-detail-row { margin: 6px 0; font-size: 13px; }
|
||||||
|
.bcal-detail-row label { color: #999; display: inline-block; width: 100px; }
|
||||||
|
.bcal-detail-row span { color: #fff; }
|
||||||
|
.bcal-modal-btns { display: flex; gap: 10px; margin-top: 18px; }
|
||||||
|
.bcal-modal-btns a, .bcal-modal-btns button { padding: 8px 18px; border-radius: 3px; cursor: pointer; font-size: 14px; text-decoration: none; border: none; color: #fff; text-align: center; }
|
||||||
|
.bcal-btn-edit { background: #5cb85c; }
|
||||||
|
.bcal-btn-delete { background: #d9534f; }
|
||||||
|
.bcal-btn-close { background: #555; color: #fff !important; }
|
||||||
|
|
||||||
|
/* Now indicator */
|
||||||
|
.bcal-now-line { position: absolute; left: 0; right: 0; border-top: 2px solid #d9534f; z-index: 4; pointer-events: none; }
|
||||||
|
.bcal-now-dot { position: absolute; left: -4px; top: -4px; width: 8px; height: 8px; background: #d9534f; border-radius: 50%; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="bcal-wrap">
|
||||||
|
<div class="pageTitle"><?php echo htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||||
|
|
||||||
|
<div class="bcal-toolbar">
|
||||||
|
<label style="color:#ccc;font-size:13px;">Dolgozó:</label>
|
||||||
|
<select id="bcal-worker-filter">
|
||||||
|
<option value="">Mindenki</option>
|
||||||
|
<?php foreach ($workers as $w): ?>
|
||||||
|
<option value="<?php echo $w->worker_id; ?>"><?php echo htmlspecialchars($w->worker_name, ENT_QUOTES, 'UTF-8'); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<a href="<?php echo SITEURL; ?>bookings">Listanézet</a>
|
||||||
|
<a href="<?php echo SITEURL; ?>bookings/calendar" class="active">Naptárnézet</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bcal-header">
|
||||||
|
<a id="bcal-prev">← Előző hét</a>
|
||||||
|
<a id="bcal-today">Ma</a>
|
||||||
|
<span class="bcal-title" id="bcal-week-label"></span>
|
||||||
|
<a id="bcal-next">Következő hét →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bcal-legend" id="bcal-legend"></div>
|
||||||
|
|
||||||
|
<div class="bcal-grid" id="bcal-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail modal -->
|
||||||
|
<div id="bcalModal" onclick="if(event.target===this)closeBcalModal()">
|
||||||
|
<div id="bcalModalBox">
|
||||||
|
<h3 id="bcalModalTitle">Foglalás részletei</h3>
|
||||||
|
<div id="bcalModalBody"></div>
|
||||||
|
<div class="bcal-modal-btns">
|
||||||
|
<a id="bcalEditBtn" class="bcal-btn-edit" href="#">Szerkesztés</a>
|
||||||
|
<a id="bcalDeleteBtn" class="bcal-btn-delete" href="#">Törlés</a>
|
||||||
|
<a class="bcal-btn-close" onclick="closeBcalModal()" style="cursor:pointer;">Bezárás</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var SITEURL = '<?php echo SITEURL; ?>';
|
||||||
|
var workerColors = <?php echo json_encode($workerColorMap); ?>;
|
||||||
|
var workersArr = <?php echo json_encode(array_map(function($w){ return array('worker_id'=>$w->worker_id,'worker_name'=>$w->worker_name); }, $workers)); ?>;
|
||||||
|
var currentMonday = '<?php echo $weekStart; ?>';
|
||||||
|
var allBookings = <?php echo json_encode($bookingsJson); ?>;
|
||||||
|
var DAY_START = 8; // 08:00
|
||||||
|
var DAY_END = 19; // 19:00
|
||||||
|
var PX_PER_HOUR = 60;
|
||||||
|
var TOTAL_HOURS = DAY_END - DAY_START;
|
||||||
|
var dayNames = ['Hétfő','Kedd','Szerda','Csütörtök','Péntek','Szombat','Vasárnap'];
|
||||||
|
|
||||||
|
function dateAdd(dateStr, days) {
|
||||||
|
var parts = dateStr.split('-');
|
||||||
|
var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
d.setDate(d.getDate() + days);
|
||||||
|
var y = d.getFullYear();
|
||||||
|
var m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
var dd = String(d.getDate()).padStart(2, '0');
|
||||||
|
return y + '-' + m + '-' + dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
var parts = dateStr.split('-');
|
||||||
|
return parts[1] + '.' + parts[2] + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateFull(dateStr) {
|
||||||
|
var parts = dateStr.split('-');
|
||||||
|
return parts[0] + '. ' + monthNames[parseInt(parts[1])-1] + ' ' + parseInt(parts[2]) + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
var monthNames = ['január','február','március','április','május','június','július','augusztus','szeptember','október','november','december'];
|
||||||
|
|
||||||
|
function getToday() {
|
||||||
|
var d = new Date();
|
||||||
|
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonday(dateStr) {
|
||||||
|
var parts = dateStr.split('-');
|
||||||
|
var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
var dow = d.getDay(); // 0=Sun
|
||||||
|
var diff = dow === 0 ? -6 : 1 - dow;
|
||||||
|
d.setDate(d.getDate() + diff);
|
||||||
|
var y = d.getFullYear();
|
||||||
|
var m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
var dd = String(d.getDate()).padStart(2, '0');
|
||||||
|
return y + '-' + m + '-' + dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeToMinutes(t) {
|
||||||
|
var p = t.split(':');
|
||||||
|
return parseInt(p[0]) * 60 + parseInt(p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLegend() {
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < workersArr.length; i++) {
|
||||||
|
var wid = String(workersArr[i].worker_id);
|
||||||
|
var color = workerColors[wid] || '#666';
|
||||||
|
html += '<div class="bcal-legend-item"><span class="bcal-legend-dot" style="background:'+color+';"></span> '+esc(workersArr[i].worker_name)+'</div>';
|
||||||
|
}
|
||||||
|
document.getElementById('bcal-legend').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWeekLabel() {
|
||||||
|
var sundayStr = dateAdd(currentMonday, 6);
|
||||||
|
var mParts = currentMonday.split('-');
|
||||||
|
var sParts = sundayStr.split('-');
|
||||||
|
var label = mParts[0] + '. ' + monthNames[parseInt(mParts[1])-1] + ' ' + parseInt(mParts[2]) + '. – ' +
|
||||||
|
(mParts[0] !== sParts[0] ? sParts[0] + '. ' : '') +
|
||||||
|
(mParts[1] !== sParts[1] ? monthNames[parseInt(sParts[1])-1] + ' ' : '') +
|
||||||
|
parseInt(sParts[2]) + '.';
|
||||||
|
document.getElementById('bcal-week-label').textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeOverlapColumns(dayBookings) {
|
||||||
|
// Sort by start time
|
||||||
|
dayBookings.sort(function(a,b) {
|
||||||
|
return timeToMinutes(a.booking_start_time) - timeToMinutes(b.booking_start_time);
|
||||||
|
});
|
||||||
|
|
||||||
|
var columns = []; // array of arrays; each column has bookings
|
||||||
|
for (var i = 0; i < dayBookings.length; i++) {
|
||||||
|
var bk = dayBookings[i];
|
||||||
|
var bkStart = timeToMinutes(bk.booking_start_time);
|
||||||
|
var bkEnd = timeToMinutes(bk.booking_finish_time);
|
||||||
|
var placed = false;
|
||||||
|
for (var c = 0; c < columns.length; c++) {
|
||||||
|
var lastInCol = columns[c][columns[c].length - 1];
|
||||||
|
var lastEnd = timeToMinutes(lastInCol.booking_finish_time);
|
||||||
|
if (bkStart >= lastEnd) {
|
||||||
|
columns[c].push(bk);
|
||||||
|
bk._col = c;
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!placed) {
|
||||||
|
bk._col = columns.length;
|
||||||
|
columns.push([bk]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine max overlapping columns for each booking
|
||||||
|
for (var i = 0; i < dayBookings.length; i++) {
|
||||||
|
var bk = dayBookings[i];
|
||||||
|
var bkStart = timeToMinutes(bk.booking_start_time);
|
||||||
|
var bkEnd = timeToMinutes(bk.booking_finish_time);
|
||||||
|
var maxCols = 1;
|
||||||
|
for (var j = 0; j < dayBookings.length; j++) {
|
||||||
|
if (i === j) continue;
|
||||||
|
var oStart = timeToMinutes(dayBookings[j].booking_start_time);
|
||||||
|
var oEnd = timeToMinutes(dayBookings[j].booking_finish_time);
|
||||||
|
if (oStart < bkEnd && oEnd > bkStart) {
|
||||||
|
maxCols++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bk._totalCols = Math.max(maxCols, columns.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize: use actual column count for overlapping group
|
||||||
|
var totalCols = columns.length;
|
||||||
|
for (var i = 0; i < dayBookings.length; i++) {
|
||||||
|
dayBookings[i]._totalCols = totalCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayBookings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid() {
|
||||||
|
var today = getToday();
|
||||||
|
var grid = document.getElementById('bcal-grid');
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// Header row: empty corner + 7 day headers
|
||||||
|
html += '<div class="bcal-time-header"></div>';
|
||||||
|
for (var d = 0; d < 7; d++) {
|
||||||
|
var dayDate = dateAdd(currentMonday, d);
|
||||||
|
var isToday = (dayDate === today);
|
||||||
|
html += '<div class="bcal-day-header' + (isToday ? ' today' : '') + '">' + dayNames[d] + '<br>' + formatDate(dayDate) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time column + day columns
|
||||||
|
// Time column
|
||||||
|
html += '<div class="bcal-time-col">';
|
||||||
|
for (var h = DAY_START; h < DAY_END; h++) {
|
||||||
|
html += '<div class="bcal-time-label">' + String(h).padStart(2,'0') + ':00</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Group bookings by date
|
||||||
|
var bookingsByDate = {};
|
||||||
|
var filterWorker = document.getElementById('bcal-worker-filter').value;
|
||||||
|
for (var i = 0; i < allBookings.length; i++) {
|
||||||
|
var bk = allBookings[i];
|
||||||
|
if (filterWorker && String(bk.worker_id) !== filterWorker) continue;
|
||||||
|
if (!bookingsByDate[bk.booking_date]) bookingsByDate[bk.booking_date] = [];
|
||||||
|
bookingsByDate[bk.booking_date].push(bk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7 day columns
|
||||||
|
for (var d = 0; d < 7; d++) {
|
||||||
|
var dayDate = dateAdd(currentMonday, d);
|
||||||
|
var isToday = (dayDate === today);
|
||||||
|
html += '<div class="bcal-day-col" style="height:'+(TOTAL_HOURS * PX_PER_HOUR)+'px;">';
|
||||||
|
|
||||||
|
// Hour lines
|
||||||
|
for (var h = 0; h < TOTAL_HOURS; h++) {
|
||||||
|
html += '<div class="bcal-hour-line" style="top:'+(h * PX_PER_HOUR)+'px;"></div>';
|
||||||
|
html += '<div class="bcal-half-line" style="top:'+(h * PX_PER_HOUR + 30)+'px;"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now line
|
||||||
|
if (isToday) {
|
||||||
|
var now = new Date();
|
||||||
|
var nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||||||
|
var nowOffset = nowMinutes - DAY_START * 60;
|
||||||
|
if (nowOffset >= 0 && nowOffset <= TOTAL_HOURS * 60) {
|
||||||
|
html += '<div class="bcal-now-line" style="top:'+nowOffset+'px;"><div class="bcal-now-dot"></div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bookings
|
||||||
|
var dayBks = bookingsByDate[dayDate] || [];
|
||||||
|
if (dayBks.length > 0) {
|
||||||
|
dayBks = computeOverlapColumns(dayBks);
|
||||||
|
var totalCols = dayBks[0]._totalCols || 1;
|
||||||
|
for (var b = 0; b < dayBks.length; b++) {
|
||||||
|
var bk = dayBks[b];
|
||||||
|
var startMin = timeToMinutes(bk.booking_start_time);
|
||||||
|
var endMin = timeToMinutes(bk.booking_finish_time);
|
||||||
|
var top = (startMin - DAY_START * 60);
|
||||||
|
var height = Math.max(endMin - startMin, 15);
|
||||||
|
var color = workerColors[String(bk.worker_id)] || '#666';
|
||||||
|
var col = bk._col || 0;
|
||||||
|
var widthPct = (100 / totalCols);
|
||||||
|
var leftPct = col * widthPct;
|
||||||
|
|
||||||
|
var timeLabel = bk.booking_start_time.substring(0,5) + '–' + bk.booking_finish_time.substring(0,5);
|
||||||
|
html += '<div class="bcal-booking" onclick="openBcalModal('+b+',\''+dayDate+'\')" style="top:'+top+'px;height:'+height+'px;background:'+color+';width:calc('+widthPct+'% - 2px);left:calc('+leftPct+'% + 1px);">';
|
||||||
|
html += '<div class="bcal-b-time">'+esc(timeLabel)+'</div>';
|
||||||
|
html += '<div class="bcal-b-name">'+esc(bk.guest_name)+'</div>';
|
||||||
|
if (height > 30) html += '<div class="bcal-b-svc">'+esc(bk.service_names_joined)+'</div>';
|
||||||
|
if (height > 45) html += '<div class="bcal-b-worker">'+esc(bk.worker_name)+'</div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = html;
|
||||||
|
|
||||||
|
// Store day bookings globally for modal access
|
||||||
|
window._bcalByDate = bookingsByDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.openBcalModal = function(idx, dateStr) {
|
||||||
|
var dayBks = window._bcalByDate[dateStr] || [];
|
||||||
|
var bk = dayBks[idx];
|
||||||
|
if (!bk) return;
|
||||||
|
|
||||||
|
var body = '';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Vendég:</label> <span>'+esc(bk.guest_name)+'</span></div>';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Email:</label> <span>'+esc(bk.guest_email)+'</span></div>';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Telefon:</label> <span>'+esc(bk.guest_phone)+'</span></div>';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Dolgozó:</label> <span>'+esc(bk.worker_name)+'</span></div>';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Dátum:</label> <span>'+esc(bk.booking_date)+'</span></div>';
|
||||||
|
body += '<div class="bcal-detail-row"><label>Időpont:</label> <span>'+bk.booking_start_time.substring(0,5)+' – '+bk.booking_finish_time.substring(0,5)+'</span></div>';
|
||||||
|
|
||||||
|
if (bk.services && bk.services.length > 0) {
|
||||||
|
body += '<div class="bcal-detail-row"><label>Szolgáltatás:</label></div>';
|
||||||
|
var totalPrice = 0;
|
||||||
|
for (var s = 0; s < bk.services.length; s++) {
|
||||||
|
body += '<div style="margin-left:105px;font-size:13px;color:#ccc;">'+esc(bk.services[s].service_name)+' — '+bk.services[s].service_price+' kr</div>';
|
||||||
|
totalPrice += parseInt(bk.services[s].service_price) || 0;
|
||||||
|
}
|
||||||
|
body += '<div class="bcal-detail-row"><label>Összesen:</label> <span>'+totalPrice+' kr</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('bcalModalBody').innerHTML = body;
|
||||||
|
document.getElementById('bcalModalTitle').textContent = 'Foglalás #' + bk.booking_id;
|
||||||
|
document.getElementById('bcalEditBtn').href = SITEURL + 'bookings/update-booking/' + bk.booking_id;
|
||||||
|
document.getElementById('bcalDeleteBtn').href = '#';
|
||||||
|
document.getElementById('bcalDeleteBtn').onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (confirm('Biztosan törölni szeretnéd ezt a foglalást?')) {
|
||||||
|
window.location.href = SITEURL + 'bookings/booking-process?delete-booking=' + bk.booking_id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.getElementById('bcalModal').style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeBcalModal = function() {
|
||||||
|
document.getElementById('bcalModal').style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadWeek(mondayStr) {
|
||||||
|
currentMonday = mondayStr;
|
||||||
|
updateWeekLabel();
|
||||||
|
|
||||||
|
var filterWorker = document.getElementById('bcal-worker-filter').value;
|
||||||
|
var url = SITEURL + 'bookings/calendar-data?week=' + mondayStr;
|
||||||
|
if (filterWorker) url += '&worker_id=' + filterWorker;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
allBookings = data;
|
||||||
|
renderGrid();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
document.getElementById('bcal-prev').onclick = function() {
|
||||||
|
loadWeek(dateAdd(currentMonday, -7));
|
||||||
|
};
|
||||||
|
document.getElementById('bcal-next').onclick = function() {
|
||||||
|
loadWeek(dateAdd(currentMonday, 7));
|
||||||
|
};
|
||||||
|
document.getElementById('bcal-today').onclick = function() {
|
||||||
|
loadWeek(getMonday(getToday()));
|
||||||
|
};
|
||||||
|
document.getElementById('bcal-worker-filter').onchange = function() {
|
||||||
|
renderGrid();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
renderLegend();
|
||||||
|
updateWeekLabel();
|
||||||
|
renderGrid();
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<div class="pageTitle"><?php echo $pageTitle;?></div>
|
<div class="pageTitle"><?php echo $pageTitle;?></div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<a href="<?php echo SITEURL;?>bookings/add-booking" class="submitBtn">Új foglalás</a>
|
<a href="<?php echo SITEURL;?>bookings/add-booking" class="submitBtn">Új foglalás</a>
|
||||||
|
<a href="<?php echo SITEURL;?>bookings/calendar" class="submitBtn">Naptárnézet</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<select id="booking-filter" class="formDropdownBox" style="padding:0">
|
<select id="booking-filter" class="formDropdownBox" style="padding:0">
|
||||||
|
|||||||
Reference in New Issue
Block a user