Files
dashboard/backend/src/models/events.model.ts
Matthias Hochmeister 5a6fc85a75 add features
2026-03-03 17:01:53 +01:00

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>;