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>
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<style>
|
||||
.cal-wrap { color: #fff; max-width: 820px; margin: 0 auto; padding: 10px; }
|
||||
.cal-header { display: flex; align-items: center; justify-content: space-between; margin: 14px 0 10px; }
|
||||
.cal-header a { color: #ccc; text-decoration: none; font-size: 18px; padding: 4px 10px; border: 1px solid #555; border-radius: 3px; }
|
||||
.cal-header a:hover { background: #333; }
|
||||
.cal-title { font-size: 20px; font-weight: bold; }
|
||||
.cal-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||
.cal-table th { background: #222; text-align: center; padding: 6px 2px; font-size: 13px; border: 1px solid #444; }
|
||||
.cal-table td { border: 1px solid #333; vertical-align: top; height: 72px; padding: 4px; cursor: pointer; position: relative; font-size: 12px; }
|
||||
.cal-table td:hover { background: #1a1a1a; }
|
||||
.cal-table td.empty { cursor: default; background: #111; }
|
||||
.cal-day-num { font-weight: bold; font-size: 13px; margin-bottom: 3px; }
|
||||
.cal-badge { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #fff; margin-top: 2px; }
|
||||
.badge-vacation { background: #d9534f; }
|
||||
.badge-sick { background: #f0ad4e; color: #222; }
|
||||
.badge-custom { background: #5bc0de; color: #222; }
|
||||
.badge-other { background: #9b59b6; }
|
||||
.badge-dayoff { background: #555; }
|
||||
.badge-working { background: #5cb85c; }
|
||||
.badge-noshift { background: #333; color: #888; }
|
||||
.cal-today { outline: 2px solid #fff; outline-offset: -2px; }
|
||||
|
||||
/* Legend */
|
||||
.cal-legend { display: flex; flex-wrap: wrap; gap: 10px; margin: 12px 0; font-size: 12px; }
|
||||
.cal-legend-item { display: flex; align-items: center; gap: 5px; }
|
||||
.cal-legend-dot { width: 14px; height: 14px; border-radius: 2px; display: inline-block; }
|
||||
|
||||
/* Modal */
|
||||
#calModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.65); z-index: 9999; }
|
||||
#calModalBox { background: #1e1e1e; border: 1px solid #555; border-radius: 6px; max-width: 400px; margin: 80px auto; padding: 24px; color: #fff; position: relative; }
|
||||
#calModalBox h3 { margin: 0 0 14px; font-size: 17px; }
|
||||
#calModalBox label { display: block; margin: 10px 0 3px; font-size: 13px; color: #ccc; }
|
||||
#calModalBox select, #calModalBox input[type="text"], #calModalBox input[type="date"], #calModalBox textarea {
|
||||
width: 100%; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 6px 8px;
|
||||
border-radius: 3px; font-size: 13px; box-sizing: border-box;
|
||||
}
|
||||
#calModalBox textarea { height: 60px; resize: vertical; }
|
||||
.cal-modal-btns { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.cal-btn-save { background: #5cb85c; color: #fff; border: none; padding: 8px 18px; border-radius: 3px; cursor: pointer; font-size: 14px; }
|
||||
.cal-btn-cancel { background: #555; color: #fff; border: none; padding: 8px 18px; border-radius: 3px; cursor: pointer; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
<div class="cal-wrap">
|
||||
<div class="pageTitle"><?php echo $pageTitle; ?></div>
|
||||
|
||||
<!-- Worker selector -->
|
||||
<?php $monthParam = $year.'-'.str_pad($month, 2, '0', STR_PAD_LEFT); ?>
|
||||
<div style="margin: 14px 0;">
|
||||
<label style="color:#ccc; font-size:13px;">Dolgozó:</label>
|
||||
<select onchange="if(this.value) window.location='<?php echo site_url('workers/worker-calendar'); ?>/' + this.value + '?month=<?php echo $monthParam; ?>'" style="background:#2a2a2a;color:#fff;border:1px solid #555;padding:5px 8px;border-radius:3px;font-size:13px;">
|
||||
<option value="">-- Válassz dolgozót --</option>
|
||||
<?php foreach ($workers as $w): ?>
|
||||
<option value="<?php echo $w->worker_id; ?>"
|
||||
<?php echo ($selectedWorker && $selectedWorker->worker_id == $w->worker_id) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($w->worker_name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedWorker):
|
||||
// Build schedule map: weekday (0=Sun..6=Sat) => 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;
|
||||
?>
|
||||
|
||||
<!-- Month navigation -->
|
||||
<div class="cal-header">
|
||||
<a href="<?php echo site_url('workers/worker-calendar/'.$workerId).'?month='.$prevParam; ?>">← Előző</a>
|
||||
<span class="cal-title"><?php echo htmlspecialchars($monthLabel); ?></span>
|
||||
<a href="<?php echo site_url('workers/worker-calendar/'.$workerId).'?month='.$nextParam; ?>">Következő →</a>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="cal-legend">
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#5cb85c;"></span> Munkanap</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#333;border:1px solid #555;"></span> Szabad nap</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#555;"></span> Napiszabadság</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#d9534f;"></span> Szabadság</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#f0ad4e;"></span> Betegszabadság</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#5bc0de;"></span> Egyedi munkaidő</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#9b59b6;"></span> Egyéb</div>
|
||||
<div class="cal-legend-item"><span class="cal-legend-dot" style="background:#888;"></span> Ebédszünet (auto)</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar grid -->
|
||||
<table class="cal-table">
|
||||
<tr>
|
||||
<th>H</th><th>K</th><th>Sz</th><th>Cs</th><th>P</th><th>Sz</th><th>V</th>
|
||||
</tr>
|
||||
<?php
|
||||
$cell = 0;
|
||||
$day = 1;
|
||||
$phpDow = [1,2,3,4,5,6,0]; // Mon=0 -> PHP weekday=1, ... Sun=6 -> PHP=0
|
||||
echo '<tr>';
|
||||
// Empty cells before first day
|
||||
for ($i = 0; $i < $startOffset; $i++) {
|
||||
echo '<td class="empty"></td>';
|
||||
$cell++;
|
||||
}
|
||||
while ($day <= $daysInMonth) {
|
||||
if ($cell % 7 === 0 && $cell > 0) {
|
||||
echo '</tr><tr>';
|
||||
}
|
||||
$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 = '<span class="cal-badge badge-vacation">Szabadság</span>';
|
||||
$cellTitle = 'Szabadság';
|
||||
} elseif ($absType === 'sick') {
|
||||
$badgeHtml = '<span class="cal-badge badge-sick">Beteg</span>';
|
||||
$cellTitle = 'Betegszabadság';
|
||||
} elseif ($absType === 'custom') {
|
||||
$st = substr($override->start_time, 0, 5);
|
||||
$et = substr($override->end_time, 0, 5);
|
||||
$badgeHtml = '<span class="cal-badge badge-custom">'.$st.'–'.$et.'</span>';
|
||||
$cellTitle = 'Egyedi: '.$st.'–'.$et;
|
||||
} elseif ($absType === 'other') {
|
||||
$badgeHtml = '<span class="cal-badge badge-other">Egyéb</span>';
|
||||
$cellTitle = 'Egyéb';
|
||||
} else {
|
||||
// is_day_off=1, no absence_type = plain day off
|
||||
$badgeHtml = '<span class="cal-badge badge-dayoff">Szabad</span>';
|
||||
$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 = '<span class="cal-badge badge-working">'.$st.'–'.$et.'</span>';
|
||||
$cellTitle = 'Munkanap: '.$st.'–'.$et;
|
||||
} else {
|
||||
$badgeHtml = '<span class="cal-badge badge-noshift">Szabad</span>';
|
||||
$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 '<td class="'.$todayClass.'" title="'.htmlspecialchars($cellTitle).'" onclick="openModal('.$encodedDate.', '.$encodedWid.', '.$encodedOver.')">';
|
||||
echo '<div class="cal-day-num">'.$day.'</div>';
|
||||
echo $badgeHtml;
|
||||
if (isset($lunchByDate[$dateStr])) {
|
||||
$lb = $lunchByDate[$dateStr];
|
||||
echo '<span class="cal-badge" style="background:#888;display:block;margin-top:2px;">🍴 '.$lb['start'].'–'.$lb['end'].'</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
|
||||
$day++;
|
||||
$cell++;
|
||||
}
|
||||
// Pad remaining cells
|
||||
while ($cell % 7 !== 0) {
|
||||
echo '<td class="empty"></td>';
|
||||
$cell++;
|
||||
}
|
||||
echo '</tr>';
|
||||
?>
|
||||
</table>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="calModal" onclick="closeModalBackdrop(event)">
|
||||
<div id="calModalBox">
|
||||
<h3 id="calModalTitle">Nap szerkesztése</h3>
|
||||
<form method="POST" action="<?php echo site_url('workers/store-calendar-override'); ?>" id="calModalForm">
|
||||
<input type="hidden" name="worker_id" id="modalWorkerId" value="">
|
||||
<input type="hidden" name="start_date" id="modalStartDate" value="">
|
||||
|
||||
<label>Típus:</label>
|
||||
<select name="override_type" id="modalType" onchange="onTypeChange()">
|
||||
<option value="remove">Felülírás törlése (alap munkaidő visszaállítása)</option>
|
||||
<option value="day_off">Szabadnap</option>
|
||||
<option value="vacation">Szabadság</option>
|
||||
<option value="sick">Betegszabadság</option>
|
||||
<option value="custom">Egyedi munkaidő</option>
|
||||
<option value="other">Egyéb</option>
|
||||
</select>
|
||||
|
||||
<div id="customTimeRow" style="display:none;">
|
||||
<label>Kezdés:</label>
|
||||
<input type="text" name="start_time" id="modalStartTime" placeholder="HH:MM" pattern="[0-2][0-9]:[0-5][0-9]" inputmode="numeric">
|
||||
<label>Vége:</label>
|
||||
<input type="text" name="end_time" id="modalEndTime" placeholder="HH:MM" pattern="[0-2][0-9]:[0-5][0-9]" inputmode="numeric">
|
||||
</div>
|
||||
|
||||
<div id="noteRow" style="display:none;">
|
||||
<label>Megjegyzés:</label>
|
||||
<textarea name="note" id="modalNote"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px;">
|
||||
<label>
|
||||
<input type="checkbox" id="rangeCheck" onchange="toggleRange()" style="margin-right:5px;">
|
||||
Dátumtartományra alkalmazás
|
||||
</label>
|
||||
</div>
|
||||
<div id="rangeRow" style="display:none;">
|
||||
<label>Záró dátum:</label>
|
||||
<input type="date" name="end_date" id="modalEndDate">
|
||||
</div>
|
||||
|
||||
<div class="cal-modal-btns">
|
||||
<button type="submit" class="cal-btn-save">Mentés</button>
|
||||
<button type="button" class="cal-btn-cancel" onclick="closeModal()">Mégse</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModal(date, workerId, existing) {
|
||||
document.getElementById('modalWorkerId').value = workerId;
|
||||
document.getElementById('modalStartDate').value = date;
|
||||
document.getElementById('calModalTitle').textContent = 'Nap: ' + date;
|
||||
document.getElementById('rangeCheck').checked = false;
|
||||
document.getElementById('rangeRow').style.display = 'none';
|
||||
document.getElementById('modalEndDate').value = '';
|
||||
|
||||
var sel = document.getElementById('modalType');
|
||||
var noteEl = document.getElementById('modalNote');
|
||||
noteEl.value = '';
|
||||
|
||||
if (existing) {
|
||||
var absType = existing.absence_type;
|
||||
if (absType === 'vacation') {
|
||||
sel.value = 'vacation';
|
||||
} else if (absType === 'sick') {
|
||||
sel.value = 'sick';
|
||||
} else if (absType === 'custom') {
|
||||
sel.value = 'custom';
|
||||
} else if (absType === 'other') {
|
||||
sel.value = 'other';
|
||||
} else if (existing.is_day_off == 1) {
|
||||
sel.value = 'day_off';
|
||||
} else {
|
||||
sel.value = 'remove';
|
||||
}
|
||||
document.getElementById('modalStartTime').value = existing.start_time || '';
|
||||
document.getElementById('modalEndTime').value = existing.end_time || '';
|
||||
noteEl.value = existing.note || '';
|
||||
} else {
|
||||
sel.value = 'remove';
|
||||
document.getElementById('modalStartTime').value = '';
|
||||
document.getElementById('modalEndTime').value = '';
|
||||
}
|
||||
|
||||
onTypeChange();
|
||||
document.getElementById('calModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('calModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function closeModalBackdrop(e) {
|
||||
if (e.target === document.getElementById('calModal')) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
function onTypeChange() {
|
||||
var type = document.getElementById('modalType').value;
|
||||
document.getElementById('customTimeRow').style.display = (type === 'custom') ? 'block' : 'none';
|
||||
var showNote = (type === 'vacation' || type === 'sick' || type === 'other' || type === 'custom');
|
||||
document.getElementById('noteRow').style.display = showNote ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleRange() {
|
||||
var show = document.getElementById('rangeCheck').checked;
|
||||
document.getElementById('rangeRow').style.display = show ? 'block' : 'none';
|
||||
if (!show) document.getElementById('modalEndDate').value = '';
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user