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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc013fd6a1
commit
e9a2b4b463
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user