import { z } from 'zod'; // --------------------------------------------------------------------------- // Core DB-mapped interfaces // --------------------------------------------------------------------------- export interface VeranstaltungKategorie { id: string; name: string; beschreibung?: string | null; farbe?: string | null; icon?: string | null; zielgruppen: string[]; alle_gruppen: boolean; 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; // Recurrence fields wiederholung?: WiederholungConfig | null; wiederholung_parent_id?: string | null; } export interface WiederholungConfig { typ: 'wöchentlich' | 'zweiwöchentlich' | 'monatlich_datum' | 'monatlich_erster_wochentag' | 'monatlich_letzter_wochentag'; intervall?: number; bis: string; wochentag?: number; } /** 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; // Recurrence fields wiederholung?: WiederholungConfig | null; wiederholung_parent_id?: string | null; } // --------------------------------------------------------------------------- // 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(), zielgruppen: z.array(z.string()).optional(), alle_gruppen: z.boolean().optional(), }); export type CreateKategorieData = z.infer; export const UpdateKategorieSchema = CreateKategorieSchema.partial(); export type UpdateKategorieData = z.infer; // --------------------------------------------------------------------------- // 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(), wiederholung: z.object({ typ: z.enum(['wöchentlich', 'zweiwöchentlich', 'monatlich_datum', 'monatlich_erster_wochentag', 'monatlich_letzter_wochentag']), intervall: z.number().int().min(1).max(52).optional(), bis: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'bis muss ein Datum (YYYY-MM-DD) sein'), wochentag: z.number().int().min(0).max(6).optional(), }).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; 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; 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;