fix: agent-08 Kiosk-Härtung + 24h-Zeiteintrag-Bug

- fix: worked_minutes nutzt jetzt Sekunden statt Minuten für Overnight-Vergleich
  (end < start statt end <= start) – verhindert 24h-Anzeige bei Schnell-Stempel
  in derselben Minute (z.B. 23:34:46 → 23:34:48)
- fix: _check_arbzg() gleicher Sec-basierter Fix
- fix: KioskDeviceStatus Enum values_callable → kiosk list crasht nicht mehr
- feat: kiosk rotate-key CLI-Kommando (Status→pending, Re-Enrollment)
- feat: Kiosk-Settings in CompanyOut/CompanyUpdate Schema (require_approval,
  track_current_user, heartbeat_interval_sec)
- feat: Kiosk-Terminal-Einstellungsblock in CompanySettingsPage (🖥️)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:42:08 +02:00
parent eae0f6f9b4
commit e83a3fbbdd
7 changed files with 267 additions and 12 deletions
+87 -1
View File
@@ -55,6 +55,10 @@ export function CompanySettingsPage() {
const [pnNext, setPnNext] = useState(1)
// Mobile
const [mobileStamping, setMobileStamping] = useState(true)
// Kiosk
const [kioskRequireApproval, setKioskRequireApproval] = useState(true)
const [kioskTrackCurrentUser, setKioskTrackCurrentUser] = useState(true)
const [kioskHeartbeatIntervalSec, setKioskHeartbeatIntervalSec] = useState(30)
// Freizeitausgleich
const [fzaOverdraftAllowed, setFzaOverdraftAllowed] = useState(true)
const [fzaWarningThreshold, setFzaWarningThreshold] = useState(0)
@@ -81,10 +85,13 @@ export function CompanySettingsPage() {
setPnRequired(c.personnel_number_required ?? false)
setPnMode(c.personnel_number_mode ?? 'manual')
setPnNext(c.personnel_number_next ?? 1)
const cc = c as CompanyOut & { mobile_stamping_enabled?: boolean; overtime_overdraft_allowed?: boolean; overtime_warning_threshold_hours?: number }
const cc = c as CompanyOut & { mobile_stamping_enabled?: boolean; overtime_overdraft_allowed?: boolean; overtime_warning_threshold_hours?: number; kiosk_require_approval?: boolean; kiosk_track_current_user?: boolean; kiosk_heartbeat_interval_sec?: number }
setMobileStamping(cc.mobile_stamping_enabled ?? true)
setFzaOverdraftAllowed(cc.overtime_overdraft_allowed ?? true)
setFzaWarningThreshold(cc.overtime_warning_threshold_hours ?? 0)
setKioskRequireApproval(cc.kiosk_require_approval ?? true)
setKioskTrackCurrentUser(cc.kiosk_track_current_user ?? true)
setKioskHeartbeatIntervalSec(cc.kiosk_heartbeat_interval_sec ?? 30)
}).catch(() => {})
api.get<{ configured: boolean; created_at: string | null }>('/companies/me/busylight-token')
.then(setBlStatus)
@@ -155,6 +162,9 @@ export function CompanySettingsPage() {
mobile_stamping_enabled: mobileStamping,
overtime_overdraft_allowed: fzaOverdraftAllowed,
overtime_warning_threshold_hours: fzaWarningThreshold,
kiosk_require_approval: kioskRequireApproval,
kiosk_track_current_user: kioskTrackCurrentUser,
kiosk_heartbeat_interval_sec: kioskHeartbeatIntervalSec,
})
setCompany(updated)
setSaved(true)
@@ -562,6 +572,82 @@ export function CompanySettingsPage() {
</div>
</div>
{/* Kiosk-Terminal-Einstellungen */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
<div className="flex items-center gap-2">
<span className="text-lg">🖥</span>
<h2 className="font-semibold text-gray-700">Kiosk-Terminals</h2>
</div>
{/* kiosk_require_approval */}
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-gray-800">Neue Geräte benötigen Admin-Freigabe</p>
<p className="text-xs text-gray-500 mt-0.5">
Wenn aktiv, muss jedes neue Kiosk-Gerät erst durch einen Admin freigeschaltet werden, bevor es Stempelungen akzeptiert.
</p>
</div>
<button
type="button"
disabled={!isAdmin}
onClick={() => setKioskRequireApproval(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed ${
kioskRequireApproval ? 'bg-blue-600' : 'bg-gray-300'
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform transition-transform duration-200 ${
kioskRequireApproval ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
{/* kiosk_track_current_user */}
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-gray-800">Aktuellen Benutzer am Terminal speichern (DSGVO)</p>
<p className="text-xs text-gray-500 mt-0.5">
Wenn aktiv, wird der zuletzt eingestempelte Mitarbeiter am Gerät gespeichert. Deaktivieren für DSGVO-konformeren Betrieb.
</p>
</div>
<button
type="button"
disabled={!isAdmin}
onClick={() => setKioskTrackCurrentUser(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed ${
kioskTrackCurrentUser ? 'bg-blue-600' : 'bg-gray-300'
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform transition-transform duration-200 ${
kioskTrackCurrentUser ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
{/* kiosk_heartbeat_interval_sec */}
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-gray-800">Heartbeat-Intervall (Sekunden)</p>
<p className="text-xs text-gray-500 mt-0.5">
Wie oft Kiosk-Terminals ihren Status an den Server melden. Minimum 10s, Maximum 120s.
</p>
</div>
<input
type="number"
min={10}
max={120}
step={5}
disabled={!isAdmin}
value={kioskHeartbeatIntervalSec}
onChange={e => setKioskHeartbeatIntervalSec(Math.min(120, Math.max(10, parseInt(e.target.value) || 30)))}
className="w-20 border border-gray-300 rounded-lg px-3 py-1.5 text-sm text-right disabled:bg-gray-50 disabled:cursor-not-allowed"
/>
</div>
</div>
{/* Freizeitausgleich-Einstellungen */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
<div className="flex items-center gap-2">