Mit dem Handy scannen, dann Personalnummer + PIN eingeben und ein-/ausstempeln.
- `)
- w.document.close()
- }
-
async function rotateBusylightToken() {
setBlBusy(true)
setError(null)
@@ -247,7 +179,6 @@ export function CompanySettingsPage() {
kiosk_require_approval: kioskRequireApproval,
kiosk_track_current_user: kioskTrackCurrentUser,
kiosk_heartbeat_interval_sec: kioskHeartbeatIntervalSec,
- public_stamp_enabled: psEnabled,
overtime_cap_hours: overtimeCapEnabled ? overtimeCapHours : null,
overtime_expiry_enabled: overtimeExpiryEnabled,
overtime_expiry_month: overtimeExpiryMonth,
@@ -641,103 +572,7 @@ export function CompanySettingsPage() {
)}
- {/* Öffentliches QR-Stempeln */}
- {isAdmin && (
-
-
- 📲
-
QR-Stempeln (Handy)
-
-
- Statischer QR-Code zum Aushängen am Eingang. Mitarbeiter scannen mit dem privaten Handy,
- melden sich per Personalnummer + PIN an und stempeln ein/aus.
-
-
-
- ⚠ Schwächer als Kiosk-Terminals: keine Geräte-Signatur. Schutz nur über PIN + Sperre nach
- Fehlversuchen. Nur aktivieren, wenn benötigt.
-
-
- {/* Toggle aktiv */}
-
-
-
QR-Stempeln aktivieren
-
Speichern nicht vergessen.
-
-
setPsEnabled(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 ${
- psEnabled ? 'bg-blue-600' : 'bg-gray-300'
- }`}
- >
-
-
-
-
-
- {psStatus?.configured ? (
-
-
- QR-Code aktiv
- {psStatus.created_at && (
- (seit {new Date(psStatus.created_at).toLocaleString('de-DE')})
- )}
-
-
- ) : (
-
-
- Kein QR-Code generiert
-
- )}
-
-
- {psUrl && (
-
-
- ⚠ QR-Code jetzt drucken/sichern – die URL wird nur einmal angezeigt.
-
- {psQr && (
-
-
-
- )}
-
- e.currentTarget.select()}
- className="flex-1 font-mono text-xs px-3 py-2 border border-amber-300 rounded bg-white" />
-
- {psCopied ? '✓ Kopiert' : 'Kopieren'}
-
-
-
- 🖨 QR-Code drucken
-
-
- )}
-
-
-
- {psStatus?.configured ? 'Neuen QR-Code generieren' : 'QR-Code generieren'}
-
- {psStatus?.configured && (
-
- Deaktivieren
-
- )}
- {psStatus?.configured && (
- Beim Neugenerieren wird der bisherige QR-Code ungültig.
- )}
-
-
- )}
+ {/* QR-Stempeln wurde in eine eigene Seite ausgelagert: /settings/qr-stamp */}
{/* Mobile-Einstellungen */}
diff --git a/frontend/src/pages/QrStampSettingsPage.tsx b/frontend/src/pages/QrStampSettingsPage.tsx
new file mode 100644
index 0000000..06b4f80
--- /dev/null
+++ b/frontend/src/pages/QrStampSettingsPage.tsx
@@ -0,0 +1,247 @@
+import { useEffect, useState } from 'react'
+import QRCode from 'qrcode'
+import { api } from '../api/client'
+import { Layout } from '../components/Layout'
+
+interface UserOut {
+ id: string; first_name: string; last_name: string; email: string; role: string
+}
+
+export function QrStampSettingsPage() {
+ const [me, setMe] = useState
(null)
+ const [companyName, setCompanyName] = useState('')
+
+ const [psEnabled, setPsEnabled] = useState(false)
+ const [psStatus, setPsStatus] = useState<{ configured: boolean; created_at: string | null } | null>(null)
+ const [psUrl, setPsUrl] = useState(null)
+ const [psQr, setPsQr] = useState(null)
+ const [psBusy, setPsBusy] = useState(false)
+ const [psCopied, setPsCopied] = useState(false)
+
+ const [saving, setSaving] = useState(false)
+ const [saved, setSaved] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ api.get('/auth/me').then(setMe).catch(() => {})
+ api.get<{ name: string }>('/companies/me').then(c => setCompanyName(c.name)).catch(() => {})
+ api.get<{ enabled: boolean; configured: boolean; created_at: string | null }>('/companies/me/public-stamp-token')
+ .then(s => { setPsStatus({ configured: s.configured, created_at: s.created_at }); setPsEnabled(s.enabled) })
+ .catch(() => {})
+ }, [])
+
+ async function saveEnabled(next: boolean) {
+ setSaving(true)
+ setError(null)
+ setSaved(false)
+ try {
+ await api.patch('/companies/me', { public_stamp_enabled: next })
+ setPsEnabled(next)
+ setSaved(true)
+ setTimeout(() => setSaved(false), 3000)
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ async function rotatePublicStampToken() {
+ setPsBusy(true)
+ setError(null)
+ try {
+ const res = await api.post<{ token: string; public_url: string; created_at: string }>(
+ '/companies/me/public-stamp-token/rotate', {}
+ )
+ setPsUrl(res.public_url)
+ setPsStatus({ configured: true, created_at: res.created_at })
+ setPsCopied(false)
+ setPsQr(await QRCode.toDataURL(res.public_url, { width: 320, margin: 2, errorCorrectionLevel: 'H' }))
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : 'Fehler beim Generieren des QR-Tokens')
+ } finally {
+ setPsBusy(false)
+ }
+ }
+
+ async function revokePublicStampToken() {
+ if (!confirm('QR-Code wirklich deaktivieren? Bestehende ausgedruckte Codes und eingerichtete Tablets funktionieren danach nicht mehr.')) return
+ setPsBusy(true)
+ setError(null)
+ try {
+ await api.del('/companies/me/public-stamp-token')
+ setPsStatus({ configured: false, created_at: null })
+ setPsUrl(null)
+ setPsQr(null)
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : 'Fehler beim Deaktivieren')
+ } finally {
+ setPsBusy(false)
+ }
+ }
+
+ async function copyPublicStampUrl() {
+ if (!psUrl) return
+ try {
+ await navigator.clipboard.writeText(psUrl)
+ setPsCopied(true)
+ setTimeout(() => setPsCopied(false), 2000)
+ } catch { /* clipboard nicht verfügbar */ }
+ }
+
+ function openOnThisDevice() {
+ if (!psUrl) return
+ window.open(psUrl, '_blank', 'noopener')
+ }
+
+ function printPublicStampQr() {
+ if (!psQr) return
+ const w = window.open('', '_blank', 'width=600,height=800')
+ if (!w) return
+ w.document.write(`QR-Stempel
+
+ Zeiterfassung – ${companyName || ''}
+
+ Mit dem Handy scannen, dann Personalnummer + PIN eingeben und ein-/ausstempeln.
+ `)
+ w.document.close()
+ }
+
+ const isAdmin = me?.role === 'COMPANY_ADMIN' || me?.role === 'SUPER_ADMIN'
+
+ return (
+
+
+
QR-Stempeln (Handy & Tablet)
+
+ {!isAdmin && (
+
+ Diese Einstellung ist nur für Firmen-Administratoren verfügbar.
+
+ )}
+
+ {error && (
+
{error}
+ )}
+ {saved && (
+
Gespeichert.
+ )}
+
+ {isAdmin && (
+
+
+ 📲
+
QR-Stempeln
+
+
+ Statischer Zugang zum Ein-/Ausstempeln. Mitarbeiter scannen den QR-Code mit dem privaten Handy
+ oder ihr richtet ein Tablet am Eingang dauerhaft auf der Stempel-Seite ein.
+ Anmeldung jeweils per Personalnummer + PIN.
+
+
+
+ ⚠ Schwächer als Kiosk-Terminals: keine Geräte-Signatur. Schutz nur über PIN + Sperre nach
+ Fehlversuchen. Nur aktivieren, wenn benötigt.
+
+
+ {/* Toggle aktiv (eigenes Speichern) */}
+
+
+
QR-Stempeln aktivieren
+
Wird sofort gespeichert.
+
+
saveEnabled(!psEnabled)}
+ 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 ${
+ psEnabled ? 'bg-blue-600' : 'bg-gray-300'
+ }`}
+ >
+
+
+
+
+
+ {psStatus?.configured ? (
+
+
+ Zugang aktiv
+ {psStatus.created_at && (
+ (seit {new Date(psStatus.created_at).toLocaleString('de-DE')})
+ )}
+
+
+ ) : (
+
+
+ Kein Zugang generiert
+
+ )}
+
+
+ {psUrl && (
+
+
+ ⚠ Jetzt sichern – der Link/QR-Code wird aus Sicherheitsgründen nur einmal angezeigt.
+
+
+ {psQr && (
+
+
+
+ )}
+
+ {/* Tablet-Link */}
+
+
🖥 Tablet-Link (Direktaufruf am Eingang)
+
+ e.currentTarget.select()}
+ className="flex-1 font-mono text-xs px-3 py-2 border border-amber-300 rounded bg-white" />
+
+ {psCopied ? '✓ Kopiert' : 'Link kopieren'}
+
+
+
+ Diesen Link auf dem Tablet im Browser öffnen und als Startseite/Lesezeichen oder im
+ Kiosk-/Vollbildmodus fixieren. Derselbe Link funktioniert auch hinter dem QR-Code fürs Handy.
+
+
+ ▶ Auf diesem Gerät öffnen
+
+
+
+
+ 🖨 QR-Code drucken (Aushang)
+
+
+ )}
+
+
+
+ {psStatus?.configured ? 'Neuen Link/QR-Code generieren' : 'Link/QR-Code generieren'}
+
+ {psStatus?.configured && (
+
+ Deaktivieren
+
+ )}
+ {psStatus?.configured && (
+ Beim Neugenerieren werden der bisherige Link und QR-Code ungültig.
+ )}
+
+
+ )}
+
+
+ )
+}