391 lines
19 KiB
PHP
391 lines
19 KiB
PHP
<?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;
|
||
}
|
||
|
||
try {
|
||
staff_set_status($pdo, (int) $staff['id'], $newStatus, $note);
|
||
staff_touch_activity($pdo, (int) $staff['id']);
|
||
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'] ?? '';
|
||
|
||
// Admin: Status eines Mitarbeiters ändern
|
||
if ($action === 'set_staff_status' && $admin) {
|
||
$targetId = (int) ($_POST['staff_id'] ?? 0);
|
||
$newStatus = (string) ($_POST['status'] ?? '');
|
||
$exists = $pdo->prepare('SELECT name FROM staff WHERE id = ? AND is_active = 1');
|
||
$exists->execute([$targetId]);
|
||
$targetName = $exists->fetchColumn();
|
||
if (!$targetName || !in_array($newStatus, STAFF_AVAILABILITY_STATUSES, true)) {
|
||
flash('error', 'Ungültiger Mitarbeiter oder Status.');
|
||
} else {
|
||
staff_set_status($pdo, $targetId, $newStatus, (string) ($_POST['note'] ?? ''));
|
||
audit_log($admin['id'], "Status von Mitarbeiter #$targetId auf $newStatus gesetzt");
|
||
flash('success', 'Status von ' . $targetName . ' geändert.');
|
||
}
|
||
redirect('/staff/availability.php');
|
||
}
|
||
// Admin: Zeit bis zum automatischen Offline einstellen
|
||
if ($action === 'save_auto_offline' && $admin) {
|
||
$m = max(0, min(1440, (int) ($_POST['auto_offline_minutes'] ?? 30)));
|
||
set_setting('staff_auto_offline_minutes', (string) $m);
|
||
audit_log($admin['id'], 'Automatisch offline nach ' . ($m > 0 ? $m . ' Minuten' : 'aus') . ' gesetzt');
|
||
flash('success', $m > 0 ? "Mitarbeiter werden nach $m Minuten ohne Aktivität automatisch auf Offline gesetzt." : 'Automatisch offline ist ausgeschaltet.');
|
||
redirect('/staff/availability.php');
|
||
}
|
||
|
||
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 ───────────────────────────────────────────────────────────────
|
||
staff_run_auto_offline($pdo);
|
||
$agents = $pdo->query(
|
||
"SELECT s.*, sa.status AS avail_status, sa.auto_offline AS avail_auto, 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. «In Besprechung bis 14:00»)"
|
||
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; ?>
|
||
<?php $__la = staff_last_active_label($ag['last_active_at'] ?? null); ?>
|
||
<?php if ($__la !== '' || !empty($ag['avail_auto'])): ?><div style="font-size:.68rem;color:var(--muted);flex-shrink:0;text-align:right"><?= !empty($ag['avail_auto']) ? 'automatisch<br>offline' : '' ?><?= $__la !== '' && empty($ag['avail_auto']) ? 'aktiv ' . e($__la) : '' ?></div><?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($admin): ?>
|
||
<!-- ── Admin: Status der Mitarbeiter ändern + Auto-Offline ───────────────────── -->
|
||
<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:6px">Status der Mitarbeiter ändern (Admin)</div>
|
||
<p class="muted" style="font-size:.8rem;margin-bottom:12px">Ein von dir gesetzter Status bleibt, bis der Mitarbeiter ihn selbst ändert oder du ihn erneut änderst.</p>
|
||
<table style="width:100%;border-collapse:collapse;font-size:.85rem">
|
||
<?php foreach ($agents as $ag): ?>
|
||
<tr style="border-top:1px solid var(--border)">
|
||
<td style="padding:6px 8px"><?= e($ag['name']) ?></td>
|
||
<td style="padding:6px 8px">
|
||
<form method="post" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="set_staff_status">
|
||
<input type="hidden" name="staff_id" value="<?= (int) $ag['id'] ?>">
|
||
<select name="status" style="width:auto">
|
||
<?php foreach ($statusLabels as $sk => [$sl]): ?><option value="<?= $sk ?>" <?= ($ag['avail_status'] ?? 'offline') === $sk ? 'selected' : '' ?>><?= $sl ?></option><?php endforeach; ?>
|
||
</select>
|
||
<input type="text" name="note" value="<?= e($ag['avail_note'] ?? '') ?>" placeholder="Notiz (optional)" maxlength="120" style="width:180px">
|
||
<button type="submit" class="btn secondary">Setzen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
<form method="post" style="margin-top:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_auto_offline">
|
||
<label style="margin:0;font-weight:500">Automatisch offline nach</label>
|
||
<input type="number" name="auto_offline_minutes" min="0" max="1440" value="<?= (int) staff_auto_offline_minutes() ?>" style="width:90px"> Minuten ohne Aktivität
|
||
<button type="submit" class="btn secondary">Speichern</button>
|
||
<span class="muted" style="font-size:.78rem">0 = aus. Bei neuer Aktivität kommt der vorherige Status zurück.</span>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- ── 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'; ?>
|