Files
planer/src/lib/bereich.ts
T
Florian WildeandClaude Opus 5 e9a2b4b463 feat(bereiche): Bereiche verwalten mit Rollenprüfung und Audit
Bereiche mit Farbe, Arbeitsfenster und Tageskapazität – die erste
schreibende Funktion und damit der Beleg, dass die Kette trägt: zod prüft
jede Eingabe, die Rolle wird in der Seite und noch einmal in der Server
Action geprüft, jede Änderung hinterlässt einen Audit-Eintrag.

Die Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name
nach Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt.
Das gilt auch in verschachtelten Objekten und Listen.

Dazu die Protokollseite für die Verwaltung und die Startseite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:02:49 +02:00

101 lines
2.7 KiB
TypeScript

import { z } from 'zod'
import { ausUhrzeit } from '@/lib/zeit'
/**
* Regeln fuer Bereiche. Reine Logik, damit Formular und Server Action
* dieselbe Antwort geben.
*/
export const WOCHENTAGE = [
'Montag',
'Dienstag',
'Mittwoch',
'Donnerstag',
'Freitag',
'Samstag',
'Sonntag',
] as const
export const KAPAZITAET_FELDER = [
'kapazitaetMo',
'kapazitaetDi',
'kapazitaetMi',
'kapazitaetDo',
'kapazitaetFr',
'kapazitaetSa',
'kapazitaetSo',
] as const
export type KapazitaetFeld = (typeof KAPAZITAET_FELDER)[number]
/** Hex-Farbe mit Raute, drei- oder sechsstellig. */
export const farbeSchema = z
.string()
.trim()
.regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, 'Bitte eine Farbe wie #2563eb angeben.')
.transform((f) => f.toLowerCase())
const uhrzeitSchema = z
.string()
.trim()
.refine((w) => ausUhrzeit(w) !== null, 'Bitte eine Uhrzeit wie 08:00 angeben.')
.transform((w) => ausUhrzeit(w) as number)
/** Eine Tageskapazitaet: 0 bis 24 Stunden, in Minuten. */
const kapazitaetSchema = z.coerce
.number()
.int('Bitte ganze Minuten angeben.')
.min(0, 'Nicht weniger als 0 Minuten.')
.max(1440, 'Nicht mehr als 1440 Minuten, also 24 Stunden.')
export const bereichSchema = z
.object({
name: z
.string()
.trim()
.min(2, 'Mindestens zwei Zeichen.')
.max(40, 'Höchstens 40 Zeichen.'),
farbe: farbeSchema,
reihenfolge: z.coerce.number().int().min(0).max(999).default(0),
aktiv: z.coerce.boolean().default(true),
arbeitsfensterVon: uhrzeitSchema,
arbeitsfensterBis: uhrzeitSchema,
kapazitaetMo: kapazitaetSchema,
kapazitaetDi: kapazitaetSchema,
kapazitaetMi: kapazitaetSchema,
kapazitaetDo: kapazitaetSchema,
kapazitaetFr: kapazitaetSchema,
kapazitaetSa: kapazitaetSchema,
kapazitaetSo: kapazitaetSchema,
})
.refine((b) => b.arbeitsfensterVon < b.arbeitsfensterBis, {
path: ['arbeitsfensterBis'],
message: 'Das Arbeitsfenster muss nach seinem Beginn enden.',
})
.refine(
(b) =>
KAPAZITAET_FELDER.every(
(feld) => b[feld] <= b.arbeitsfensterBis - b.arbeitsfensterVon,
),
{
path: ['kapazitaetMo'],
message: 'Keine Tageskapazität darf länger sein als das Arbeitsfenster.',
},
)
export type BereichEingabe = z.infer<typeof bereichSchema>
/** Summe der Wochenkapazitaet in Minuten. */
export function wochenKapazitaet(bereich: Record<KapazitaetFeld, number>): number {
return KAPAZITAET_FELDER.reduce((summe, feld) => summe + bereich[feld], 0)
}
/** Kapazitaet des Wochentags, 0 = Montag. */
export function kapazitaetAmTag(
bereich: Record<KapazitaetFeld, number>,
wochentagIndex: number,
): number {
const feld = KAPAZITAET_FELDER[wochentagIndex]
return feld ? bereich[feld] : 0
}