bug fix for atemschutz

This commit is contained in:
Matthias Hochmeister
2026-03-01 19:19:12 +01:00
parent 2630224edd
commit 6495ca94d1
17 changed files with 5116 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
import { z } from 'zod';
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
export const BUCHUNGS_ARTEN = ['intern', 'extern', 'wartung', 'reservierung', 'sonstiges'] as const;
export type BuchungsArt = (typeof BUCHUNGS_ARTEN)[number];
// ---------------------------------------------------------------------------
// Core DB-mapped interfaces
// ---------------------------------------------------------------------------
export interface FahrzeugBuchung {
id: string;
fahrzeug_id: string;
titel: string;
beschreibung?: string | null;
beginn: Date;
ende: Date;
buchungs_art: BuchungsArt;
gebucht_von: string;
kontakt_person?: string | null;
kontakt_telefon?: string | null;
abgesagt: boolean;
abgesagt_grund?: string | null;
erstellt_am: Date;
aktualisiert_am: Date;
// Joined
fahrzeug_name?: string | null;
fahrzeug_kennzeichen?: string | null;
gebucht_von_name?: string | null;
}
/** Lightweight list item — used in calendar and upcoming list widget */
export interface FahrzeugBuchungListItem {
id: string;
fahrzeug_id: string;
fahrzeug_name: string;
fahrzeug_kennzeichen?: string | null;
titel: string;
buchungs_art: BuchungsArt;
beginn: Date;
ende: Date;
abgesagt: boolean;
gebucht_von_name?: string | null;
}
// ---------------------------------------------------------------------------
// Zod validation schemas
// ---------------------------------------------------------------------------
const BuchungBaseSchema = z.object({
fahrzeugId: z.string().uuid('fahrzeugId muss eine gueltige UUID sein'),
titel: z.string().min(3, 'Titel muss mindestens 3 Zeichen haben').max(500),
beschreibung: z.string().max(2000).optional().nullable(),
beginn: z
.string()
.datetime({ offset: true, message: 'beginn muss ein ISO-8601 Datum mit Zeitzone sein' })
.transform((s) => new Date(s)),
ende: z
.string()
.datetime({ offset: true, message: 'ende muss ein ISO-8601 Datum mit Zeitzone sein' })
.transform((s) => new Date(s)),
buchungsArt: z.enum(BUCHUNGS_ARTEN).default('intern'),
kontaktPerson: z.string().max(255).optional().nullable(),
kontaktTelefon: z.string().max(50).optional().nullable(),
});
export const CreateBuchungSchema = BuchungBaseSchema.refine(
(d) => d.ende > d.beginn,
{ message: 'Ende muss nach Beginn liegen', path: ['ende'] }
);
export type CreateBuchungData = z.infer<typeof CreateBuchungSchema>;
export const UpdateBuchungSchema = BuchungBaseSchema.partial().refine(
(d) => d.ende == null || d.beginn == null || d.ende > d.beginn,
{ message: 'Ende muss nach Beginn liegen', path: ['ende'] }
);
export type UpdateBuchungData = z.infer<typeof UpdateBuchungSchema>;
export const CancelBuchungSchema = z.object({
abgesagt_grund: z
.string()
.min(5, 'Bitte gib einen Stornierungsgrund an (min. 5 Zeichen)')
.max(1000),
});
export type CancelBuchungData = z.infer<typeof CancelBuchungSchema>;

View File

@@ -0,0 +1,143 @@
import { z } from 'zod';
// ---------------------------------------------------------------------------
// Core DB-mapped interfaces
// ---------------------------------------------------------------------------
export interface VeranstaltungKategorie {
id: string;
name: string;
beschreibung?: string | null;
farbe?: string | null;
icon?: string | null;
erstellt_von?: string | null;
erstellt_am: Date;
aktualisiert_am: Date;
}
export interface Veranstaltung {
id: string;
titel: string;
beschreibung?: string | null;
ort?: string | null;
ort_url?: string | null;
kategorie_id?: string | null;
datum_von: Date;
datum_bis: Date;
ganztaegig: boolean;
zielgruppen: string[];
alle_gruppen: boolean;
max_teilnehmer?: number | null;
anmeldung_erforderlich: boolean;
anmeldung_bis?: Date | null;
erstellt_von: string;
abgesagt: boolean;
abgesagt_grund?: string | null;
abgesagt_am?: Date | null;
erstellt_am: Date;
aktualisiert_am: Date;
// Joined / enriched fields
kategorie_name?: string | null;
kategorie_farbe?: string | null;
kategorie_icon?: string | null;
erstellt_von_name?: string | null;
}
/** Lightweight version for calendar and list views */
export interface VeranstaltungListItem {
id: string;
titel: string;
ort?: string | null;
kategorie_id?: string | null;
kategorie_name?: string | null;
kategorie_farbe?: string | null;
kategorie_icon?: string | null;
datum_von: Date;
datum_bis: Date;
ganztaegig: boolean;
alle_gruppen: boolean;
zielgruppen: string[];
abgesagt: boolean;
anmeldung_erforderlich: boolean;
}
// ---------------------------------------------------------------------------
// Zod schemas -- Kategorien
// ---------------------------------------------------------------------------
export const CreateKategorieSchema = z.object({
name: z
.string()
.min(2, 'Name muss mindestens 2 Zeichen haben')
.max(255),
beschreibung: z.string().max(500).optional().nullable(),
farbe: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Farbe muss ein gültiger Hex-Farbwert sein (z.B. #1976d2)')
.optional(),
icon: z.string().max(100).optional(),
});
export type CreateKategorieData = z.infer<typeof CreateKategorieSchema>;
export const UpdateKategorieSchema = CreateKategorieSchema.partial();
export type UpdateKategorieData = z.infer<typeof UpdateKategorieSchema>;
// ---------------------------------------------------------------------------
// Zod schemas -- Veranstaltungen
// ---------------------------------------------------------------------------
const VeranstaltungBaseSchema = z.object({
titel: z
.string()
.min(3, 'Titel muss mindestens 3 Zeichen haben')
.max(500),
beschreibung: z.string().max(5000).optional().nullable(),
ort: z.string().max(500).optional().nullable(),
// Plain string validator — some users use relative paths or internal URLs
ort_url: z.string().max(1000).optional().nullable(),
kategorie_id: z.string().uuid('kategorie_id muss eine gültige UUID sein').optional().nullable(),
datum_von: z
.string()
.datetime({ offset: true, message: 'datum_von muss ein ISO-8601 Datum mit Zeitzone sein' })
.transform((s) => new Date(s)),
datum_bis: z
.string()
.datetime({ offset: true, message: 'datum_bis muss ein ISO-8601 Datum mit Zeitzone sein' })
.transform((s) => new Date(s)),
ganztaegig: z.boolean().default(false),
zielgruppen: z.array(z.string()).default([]),
alle_gruppen: z.boolean().default(false),
max_teilnehmer: z.number().int().positive().optional().nullable(),
anmeldung_erforderlich: z.boolean().default(false),
anmeldung_bis: z
.string()
.datetime({ offset: true, message: 'anmeldung_bis muss ein ISO-8601 Datum mit Zeitzone sein' })
.transform((s) => new Date(s))
.optional()
.nullable(),
});
export const CreateVeranstaltungSchema = VeranstaltungBaseSchema.refine(
(d) => d.datum_bis >= d.datum_von,
{ message: 'datum_bis muss nach datum_von liegen', path: ['datum_bis'] }
);
export type CreateVeranstaltungData = z.infer<typeof CreateVeranstaltungSchema>;
export const UpdateVeranstaltungSchema = VeranstaltungBaseSchema.partial().refine(
(d) => d.datum_bis == null || d.datum_von == null || d.datum_bis >= d.datum_von,
{ message: 'datum_bis muss nach datum_von liegen', path: ['datum_bis'] }
);
export type UpdateVeranstaltungData = z.infer<typeof UpdateVeranstaltungSchema>;
export const CancelVeranstaltungSchema = z.object({
abgesagt_grund: z
.string()
.min(5, 'Bitte gib einen Grund für die Absage an (min. 5 Zeichen)')
.max(1000),
});
export type CancelVeranstaltungData = z.infer<typeof CancelVeranstaltungSchema>;