Files
studiobeve.no/application/views/admin/includes/booking-calendar-form.php
T
UbuntuandClaude Opus 4.6 6abb90329d 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>
2026-05-07 14:03:41 +00:00

437 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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">&#8592; 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 &#8594;</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>