Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:21 +00:00
parent 6cced3beda
commit de06066052
50 changed files with 10411 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
<?php
/**
* staff/availability.php
* Verfügbarkeitsstatus setzen + Schichtplan verwalten (Admin)
*/
require_once __DIR__ . '/../includes/bootstrap.php';
$staff = require_staff_login();
$pdo = get_db();
// ── AJAX: Status setzen ───────────────────────────────────────────────────────
// Status-AJAX: POST mit 'status' Feld aber ohne 'action' Feld
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['status']) && !isset($_POST['action'])) {
header('Content-Type: application/json');
// CSRF prüfen
$csrfOk = hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '');
if (!$csrfOk) { http_response_code(403); echo json_encode(['error'=>'CSRF-Fehler', 'got'=>$_POST['csrf_token']??'', 'expected'=>$_SESSION['csrf_token']??'']); exit; }
$newStatus = $_POST['status'] ?? '';
$note = trim($_POST['note'] ?? '');
$allowed = ['available', 'busy', 'break', 'offline'];
if (!in_array($newStatus, $allowed, true)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiger Status']);
exit;
}
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$availabilitySql = $drv === 'sqlite'
? 'INSERT INTO staff_availability (staff_id, status, note, updated_at) VALUES (?,?,?,?)
ON CONFLICT(staff_id) DO UPDATE SET
status=excluded.status, note=excluded.note, updated_at=excluded.updated_at'
: 'INSERT INTO staff_availability (staff_id, status, note, updated_at) VALUES (?,?,?,?)
ON DUPLICATE KEY UPDATE
status=VALUES(status), note=VALUES(note), updated_at=VALUES(updated_at)';
try {
$pdo->prepare($availabilitySql)->execute([$staff['id'], $newStatus, $note ?: null, db_now()]);
echo json_encode(['ok' => true, 'status' => $newStatus]);
} catch (Throwable $e) {
error_log('staff_availability: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Status konnte nicht gespeichert werden.']);
}
exit;
}
// ── Schichtplan (Admin-Seite) ─────────────────────────────────────────────────
$admin = $staff['role'] === 'admin' ? $staff : null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'save_shifts' && $admin) {
$targetId = (int)($_POST['staff_id'] ?? 0);
$__nameStmt = $pdo->prepare('SELECT name FROM staff WHERE id = ?');
$__nameStmt->execute([$targetId]);
$editAgentName = $__nameStmt->fetchColumn() ?: "#$targetId";
if (!$targetId) {
flash('error', 'Ungültiger Mitarbeiter.');
redirect('/staff/availability.php');
}
// Alte Schichten löschen
$pdo->prepare('DELETE FROM staff_shifts WHERE staff_id = ?')->execute([$targetId]);
// Neue speichern
$days = $_POST['days'] ?? [];
foreach ($days as $day => $times) {
if (empty($times['active'])) continue;
$start = $times['start'] ?? '08:00';
$end = $times['end'] ?? '17:00';
// Keine Zeitvalidierung – Nutzer ist verantwortlich
$pdo->prepare(
'INSERT INTO staff_shifts (staff_id, weekday, start_time, end_time, is_active) VALUES (?,?,?,?,1)'
)->execute([$targetId, (int)$day, $start, $end]);
}
audit_log($admin['id'], "Schichtplan für Mitarbeiter #$targetId gespeichert");
flash('success', 'Schichtplan für ' . e($editAgentName ?? "#$targetId") . ' gespeichert.');
redirect('/staff/availability.php?edit=' . $targetId);
}
}
// ── Daten laden ───────────────────────────────────────────────────────────────
$agents = $pdo->query(
"SELECT s.*, sa.status AS avail_status, sa.note AS avail_note, sa.updated_at AS avail_updated
FROM staff s
LEFT JOIN staff_availability sa ON sa.staff_id = s.id
WHERE s.is_active = 1
ORDER BY s.name"
)->fetchAll();
// Schichten für alle Mitarbeiter
$allShifts = $pdo->query(
'SELECT * FROM staff_shifts WHERE is_active = 1 ORDER BY staff_id, weekday, start_time'
)->fetchAll();
$shiftsByStaff = [];
foreach ($allShifts as $sh) {
$shiftsByStaff[$sh['staff_id']][$sh['weekday']][] = $sh;
}
// Für den Edit-Modus
$editStaffId = (int)($_GET['edit'] ?? 0);
$editShifts = $editStaffId ? ($shiftsByStaff[$editStaffId] ?? []) : [];
$pageTitle = 'Verfügbarkeit & Schichten';
$activePage = 'availability';
require __DIR__ . '/_header.php';
$statusLabels = [
'available' => ['Verfügbar', '#22c55e', '🟢'],
'busy' => ['Beschäftigt', '#f59e0b', '🟡'],
'break' => ['Pause', '#6366f1', '🟣'],
'offline' => ['Offline', '#94a3b8', '⚫'],
];
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
?>
<div class="topbar">
<h1>Verfügbarkeit & Schichten</h1>
</div>
<!-- Hidden CSRF form für AJAX -->
<form id="csrf-form" style="display:none"><?= csrf_field() ?></form>
<!-- ── Eigener Status ─────────────────────────────────────────────────────────── -->
<?php
$myAvail = null;
foreach ($agents as $a) { if ($a['id'] === $staff['id']) { $myAvail = $a; break; } }
$myStatus = $myAvail['avail_status'] ?? 'available';
?>
<div class="card" style="margin-bottom:20px">
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Mein Status</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<?php foreach ($statusLabels as $sk => [$sl, $sc, $se]): ?>
<button type="button" onclick="setMyStatus('<?= $sk ?>')"
class="status-btn <?= $myStatus === $sk ? 'active' : '' ?>"
data-status="<?= $sk ?>"
style="display:flex;align-items:center;gap:7px;padding:8px 16px;border:2px solid <?= $myStatus === $sk ? $sc : 'var(--border)' ?>;border-radius:8px;background:<?= $myStatus === $sk ? $sc.'18' : 'transparent' ?>;cursor:pointer;font-size:.85rem;font-weight:<?= $myStatus === $sk ? '600' : '400' ?>;color:<?= $myStatus === $sk ? $sc : 'var(--ink-soft)' ?>;transition:all .15s">
<span><?= $se ?></span> <?= $sl ?>
</button>
<?php endforeach; ?>
<div style="flex:1;min-width:160px">
<input type="text" id="avail-note" value="<?= e($myAvail['avail_note'] ?? '') ?>"
placeholder="Notiz (optional, z.B. &laquo;In Besprechung bis 14:00&raquo;)"
style="width:100%;font-size:.83rem"
oninput="scheduleStatusUpdate()">
</div>
</div>
</div>
<!-- ── Team-Übersicht ─────────────────────────────────────────────────────────── -->
<div class="card" style="margin-bottom:20px">
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:14px">Team-Übersicht</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">
<?php foreach ($agents as $ag):
$as = $ag['avail_status'] ?? 'offline';
[$asl, $asc, $ase] = $statusLabels[$as] ?? ['Unbekannt','#94a3b8','⚫'];
$hasShift = isset($shiftsByStaff[$ag['id']]);
?>
<div id="agent-status-<?= $ag['id'] ?>" style="display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--surface)">
<div style="width:36px;height:36px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:.9rem;font-weight:700;flex-shrink:0;position:relative">
<?= mb_strtoupper(mb_substr($ag['name'], 0, 1)) ?>
<span class="status-dot" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:<?= $asc ?>;border:2px solid var(--surface)"></span>
</div>
<div style="min-width:0;flex:1">
<div style="font-size:.85rem;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<?= e($ag['name']) ?>
<?php if ($ag['is_on_vacation']): ?><span style="font-size:.7rem">🌴</span><?php endif; ?>
</div>
<div style="font-size:.75rem;font-weight:500"><span class="status-label" style="color:<?= $asc ?>"><?= $asl ?></span></div>
<div class="status-note" style="font-size:.72rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?= e($ag['avail_note'] ?? '') ?></div>
</div>
<?php if ($admin): ?>
<a href="?edit=<?= $ag['id'] ?>" title="Schichtplan bearbeiten"
style="color:var(--muted);font-size:.78rem;text-decoration:none;flex-shrink:0">⚙️</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- ── Schichtplan-Editor (Admin) ─────────────────────────────────────────────── -->
<?php if ($admin && $editStaffId): ?>
<?php
$editAgent = null;
foreach ($agents as $a) { if ($a['id'] === $editStaffId) { $editAgent = $a; break; } }
?>
<?php if ($editAgent): ?>
<div class="card">
<h3>Schichtplan: <?= e($editAgent['name']) ?></h3>
<p class="muted" style="font-size:.83rem;margin-bottom:16px">
Definiere an welchen Tagen und zu welchen Zeiten dieser Mitarbeiter verfügbar ist.
Tickets werden außerhalb der Schicht nicht automatisch zugewiesen.
</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_shifts">
<input type="hidden" name="staff_id" value="<?= $editStaffId ?>">
<table style="width:100%;border-collapse:collapse">
<thead>
<tr>
<th style="text-align:left;padding:8px 12px;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Tag</th>
<th style="padding:8px 12px;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Aktiv</th>
<th style="padding:8px 12px;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Von</th>
<th style="padding:8px 12px;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Bis</th>
</tr>
</thead>
<tbody>
<?php for ($d = 1; $d <= 7; $d++):
$dayShift = $editShifts[$d][0] ?? null;
$active = $dayShift !== null;
$start = $active ? substr($dayShift['start_time'], 0, 5) : '08:00';
$end = $active ? substr($dayShift['end_time'], 0, 5) : '17:00';
?>
<tr style="border-bottom:1px solid var(--border-soft)">
<td style="padding:10px 12px;font-size:.88rem;font-weight:500"><?= $weekdays[$d-1] ?></td>
<td style="padding:10px 12px;text-align:center">
<input type="checkbox" name="days[<?= $d ?>][active]" value="1" style="width:auto"
<?= $active ? 'checked' : '' ?>
onchange="toggleDay(<?= $d ?>, this.checked)">
</td>
<td style="padding:10px 12px">
<input type="time" name="days[<?= $d ?>][start]" value="<?= e($start) ?>"
id="day-start-<?= $d ?>" style="font-size:.83rem;padding:5px 8px;width:130px"
<?= !$active ? 'disabled style="opacity:.4"' : '' ?> required>
</td>
<td style="padding:10px 12px">
<input type="time" name="days[<?= $d ?>][end]" value="<?= e($end) ?>"
id="day-end-<?= $d ?>" style="font-size:.83rem;padding:5px 8px;width:130px"
<?= !$active ? 'disabled style="opacity:.4"' : '' ?> required>
</td>
</tr>
<?php endfor; ?>
</tbody>
</table>
<div class="btn-row" style="margin-top:16px">
<button type="submit" class="btn">Speichern</button>
<a href="/staff/availability.php" class="btn secondary">Abbrechen</a>
</div>
</form>
</div>
<?php endif; ?>
<?php endif; ?>
<script>
function getCsrf() {
return document.querySelector('#csrf-form input[name=csrf_token]')?.value
|| document.querySelector('input[name=csrf_token]')?.value || '';
}
var currentStatus = '<?= e($myStatus) ?>';
var statusUpdateTimer;
function setMyStatus(s) {
currentStatus = s;
document.querySelectorAll('.status-btn').forEach(function(btn) {
var active = btn.dataset.status === s;
var colors = {
available: ['#22c55e','#22c55e18'],
busy: ['#f59e0b','#f59e0b18'],
break: ['#6366f1','#6366f118'],
offline: ['#94a3b8','#94a3b818'],
};
var c = colors[btn.dataset.status] || ['var(--border)','transparent'];
btn.style.borderColor = active ? c[0] : 'var(--border)';
btn.style.background = active ? c[1] : 'transparent';
btn.style.color = active ? c[0] : 'var(--ink-soft)';
btn.style.fontWeight = active ? '600' : '400';
});
sendStatusUpdate();
}
function scheduleStatusUpdate() {
clearTimeout(statusUpdateTimer);
statusUpdateTimer = setTimeout(sendStatusUpdate, 800);
}
function sendStatusUpdate() {
var note = document.getElementById('avail-note')?.value || '';
var fd = new FormData();
fd.append('status', currentStatus);
fd.append('note', note);
fd.append('csrf_token', getCsrf());
fetch('/staff/availability.php', {
method: 'POST',
body: fd,
credentials: 'same-origin'
}).then(function(r) {
return r.json();
}).then(function(data) {
if (data.error) console.error('Status-Update Fehler:', data.error);
}).catch(function(err) {
console.error('Status-Update fehlgeschlagen:', err);
});
}
// ── Team-Status Live-Update ──────────────────────────────────────────────
function refreshTeamStatus() {
fetch('/staff/availability_status.php', { credentials: 'same-origin' })
.then(function(r){ return r.json(); })
.then(function(data) {
data.forEach(function(ag) {
var el = document.getElementById('agent-status-' + ag.id);
if (!el) return;
var colors = { available:'#22c55e', busy:'#f59e0b', break:'#6366f1', offline:'#94a3b8' };
var labels = { available:'Verfügbar', busy:'Beschäftigt', break:'Pause', offline:'Offline' };
var s = ag.is_on_vacation ? 'offline' : (ag.status || 'offline');
var label = ag.is_on_vacation ? '🌴 Urlaub' : labels[s];
var color = colors[s] || '#94a3b8';
el.querySelector('.status-dot').style.background = color;
el.querySelector('.status-label').textContent = label;
el.querySelector('.status-label').style.color = color;
var noteEl = el.querySelector('.status-note');
if (noteEl) noteEl.textContent = ag.note || '';
});
}).catch(function(){});
}
// Alle 15 Sekunden Team-Status aktualisieren
setInterval(refreshTeamStatus, 15000);
// Nach eigenem Status-Update sofort refreshen
var _origSendStatus = sendStatusUpdate;
sendStatusUpdate = function() {
_origSendStatus();
setTimeout(refreshTeamStatus, 500);
};
function toggleDay(d, active) {
document.getElementById('day-start-' + d).disabled = !active;
document.getElementById('day-end-' + d).disabled = !active;
}
</script>
<?php require __DIR__ . '/_footer.php'; ?>