Initial commit – TimeMaster Zeiterfassung & HR-Tool

Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
co-authored by Claude Sonnet 4.6
commit 45218cc744
177 changed files with 29145 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
const BASE_URL = '/api/v1'
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = localStorage.getItem('access_token')
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
}
if (token) headers['Authorization'] = `Bearer ${token}`
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers })
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }))
const msg = typeof err.detail === 'string'
? err.detail
: Array.isArray(err.detail)
? err.detail.map((e: { msg: string }) => e.msg).join(', ')
: res.statusText
throw new Error(msg)
}
if (res.status === 204) return undefined as T
return res.json()
}
async function requestForm<T>(path: string, form: FormData): Promise<T> {
const token = localStorage.getItem('access_token')
const headers: Record<string, string> = {}
if (token) headers['Authorization'] = `Bearer ${token}`
// No Content-Type header browser sets multipart boundary automatically
const res = await fetch(`${BASE_URL}${path}`, { method: 'POST', body: form, headers })
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }))
const msg = typeof err.detail === 'string'
? err.detail
: Array.isArray(err.detail)
? err.detail.map((e: { msg: string }) => e.msg).join(', ')
: res.statusText
throw new Error(msg)
}
if (res.status === 204) return undefined as T
return res.json()
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) => request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
patch: <T>(path: string, body: unknown) => request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
del: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
postForm: <T>(path: string, form: FormData) => requestForm<T>(path, form),
}