Upload via GUI (360 Dateien)

This commit is contained in:
2026-10-30 21:50:04 +00:00
parent 91ae4659b5
commit fa7cb047a9
6 changed files with 879 additions and 322 deletions
+66 -69
View File
@@ -125,17 +125,36 @@ $activePage = 'availability';
require __DIR__ . '/_header.php';
$statusLabels = [
'available' => ['Verfügbar', '#22c55e', '🟢'],
'busy' => ['Beschäftigt', '#f59e0b', '🟡'],
'break' => ['Pause', '#6366f1', '🟣'],
'offline' => ['Offline', '#94a3b8', '⚫'],
'available' => ['Verfügbar', '#22c55e'],
'busy' => ['Beschäftigt', '#f59e0b'],
'break' => ['Pause', '#6366f1'],
'offline' => ['Offline', '#94a3b8'],
];
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
?>
<style>
.av-dot { display:inline-block; width:10px; height:10px; border-radius:50%; background:var(--c, #94a3b8); flex:none }
.av-status { display:flex; gap:10px; flex-wrap:wrap; align-items:center }
.status-btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-size:.88rem; font-weight:600; padding:8px 16px; border:1px solid var(--border); border-radius:8px; background:var(--surface); color:var(--ink-soft); cursor:pointer; transition:border-color .15s, background .15s, color .15s }
.status-btn:hover { border-color:var(--c) }
.status-btn.active { border-color:var(--c); background:color-mix(in srgb, var(--c) 12%, var(--surface)); color:var(--ink) }
.av-note { flex:1; min-width:200px; margin:0 }
.av-team { display:grid; grid-template-columns:repeat(auto-fill, minmax(250px, 1fr)); gap:12px }
.av-agent { display:flex; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--border); border-radius:10px; background:var(--surface) }
.av-avatar { position:relative; width:38px; height:38px; border-radius:50%; background:var(--primary); color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; flex:none }
.av-avatar .status-dot { position:absolute; right:-2px; bottom:-2px; width:12px; height:12px; border-radius:50%; border:2px solid var(--surface); background:#94a3b8 }
.av-agent .av-name { font-weight:600; overflow-wrap:anywhere }
.av-agent .status-label { font-size:.8rem; font-weight:600 }
.av-agent .status-note, .av-agent .av-seen { font-size:.76rem; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.av-edit { margin-left:auto; flex:none; padding:4px 10px; font-size:.78rem }
.av-admin-row form { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin:0 }
.av-admin-row select, .av-admin-row input[type=text] { margin:0; width:auto }
.av-auto { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:16px; padding-top:16px; border-top:1px solid var(--border-soft) }
.av-auto label, .av-auto input { margin:0 }
.av-auto input[type=number] { width:90px }
</style>
<div class="topbar">
<h1>Verfügbarkeit & Schichten</h1>
</div>
<h1 style="margin-top:0">Verfügbarkeit &amp; Schichten</h1>
<!-- Hidden CSRF form für AJAX -->
<form id="csrf-form" style="display:none"><?= csrf_field() ?></form>
@@ -147,53 +166,38 @@ 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 ?>
<h3>Mein Status</h3>
<div class="av-status">
<?php foreach ($statusLabels as $sk => [$sl, $sc]): ?>
<button type="button" onclick="setMyStatus('<?= $sk ?>')" class="status-btn <?= $myStatus === $sk ? 'active' : '' ?>" data-status="<?= $sk ?>" style="--c:<?= $sc ?>">
<span class="av-dot" style="--c:<?= $sc ?>"></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>
<input type="text" id="avail-note" class="av-note" value="<?= e($myAvail['avail_note'] ?? '') ?>" maxlength="120"
placeholder="Notiz (optional, z. B. „In Besprechung bis 14:00“)" oninput="scheduleStatusUpdate()">
</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">
<h3>Team-Übersicht</h3>
<div class="av-team">
<?php foreach ($agents as $ag):
$as = $ag['avail_status'] ?? 'offline';
[$asl, $asc, $ase] = $statusLabels[$as] ?? ['Unbekannt','#94a3b8','⚫'];
$hasShift = isset($shiftsByStaff[$ag['id']]);
[$asl, $asc] = $statusLabels[$as] ?? ['Unbekannt', '#94a3b8'];
if (!empty($ag['is_on_vacation'])) { $asl = 'Urlaub'; $asc = '#94a3b8'; }
$__la = staff_last_active_label($ag['last_active_at'] ?? null);
?>
<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 class="av-agent" id="agent-status-<?= $ag['id'] ?>">
<div class="av-avatar"><?= e(mb_strtoupper(mb_substr($ag['name'], 0, 1))) ?><span class="status-dot" style="background:<?= $asc ?>"></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 class="av-name"><?= e($ag['name']) ?></div>
<div><span class="status-label" style="color:<?= $asc ?>"><?= $asl ?></span></div>
<div class="status-note"><?= e($ag['avail_note'] ?? '') ?></div>
<?php if (!empty($ag['avail_auto'])): ?><div class="av-seen">automatisch offline</div>
<?php elseif ($__la !== ''): ?><div class="av-seen">aktiv <?= e($__la) ?></div><?php endif; ?>
</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; ?>
<?php if ($admin): ?><a href="?edit=<?= $ag['id'] ?>" class="btn secondary av-edit" title="Schichtplan bearbeiten">Schichtplan</a><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
@@ -202,34 +206,38 @@ $myStatus = $myAvail['avail_status'] ?? 'available';
<?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">
<h3>Status der Mitarbeiter ändern (Admin)</h3>
<p class="muted" style="font-size:.85rem;margin:0 0 12px">Ein von dir gesetzter Status bleibt, bis der Mitarbeiter ihn selbst ändert oder du ihn erneut änderst.</p>
<div style="overflow-x:auto"><table>
<thead><tr><th>Mitarbeiter</th><th>Status und Notiz</th></tr></thead>
<tbody>
<?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">
<tr class="av-admin-row">
<td><strong><?= e($ag['name']) ?></strong></td>
<td>
<form method="post">
<?= 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">
<select name="status">
<?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">
<input type="text" name="note" value="<?= e($ag['avail_note'] ?? '') ?>" placeholder="Notiz (optional)" maxlength="120" style="width:220px">
<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">
</tbody>
</table></div>
<form method="post" class="av-auto">
<?= 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
<label>Automatisch offline nach</label>
<input type="number" name="auto_offline_minutes" min="0" max="1440" value="<?= (int) staff_auto_offline_minutes() ?>">
<span>Minuten ohne Aktivität</span>
<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>
<span class="muted" style="font-size:.8rem">0 = aus. Bei neuer Aktivität kommt der vorherige Status zurück.</span>
</form>
</div>
<?php endif; ?>
@@ -309,18 +317,7 @@ 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';
btn.classList.toggle('active', btn.dataset.status === s);
});
sendStatusUpdate();
}
@@ -360,7 +357,7 @@ function refreshTeamStatus() {
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 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;