167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
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<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(),
|
|
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<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>;
|