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

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

323 lines
16 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.
<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; ?>">&#8592; 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ő &#8594;</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;">&#x1F374; '.$lb['start'].'&ndash;'.$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>