import { z } from 'zod'; // --------------------------------------------------------------------------- // ENUMS (as const arrays for runtime use + TypeScript types) // --------------------------------------------------------------------------- export const EINSATZ_ARTEN = [ 'Brand', 'THL', 'ABC', 'BMA', 'Hilfeleistung', 'Fehlalarm', 'Brandsicherheitswache', ] as const; export type EinsatzArt = (typeof EINSATZ_ARTEN)[number]; export const EINSATZ_ART_LABELS: Record = { Brand: 'Brand', THL: 'Technische Hilfeleistung', ABC: 'ABC-Einsatz / Gefahrgut', BMA: 'Brandmeldeanlage', Hilfeleistung: 'Hilfeleistung', Fehlalarm: 'Fehlalarm', Brandsicherheitswache: 'Brandsicherheitswache', }; export const EINSATZ_STATUS = ['aktiv', 'abgeschlossen', 'archiviert'] as const; export type EinsatzStatus = (typeof EINSATZ_STATUS)[number]; export const EINSATZ_STATUS_LABELS: Record = { aktiv: 'Aktiv', abgeschlossen: 'Abgeschlossen', archiviert: 'Archiviert', }; export const EINSATZ_FUNKTIONEN = [ 'Einsatzleiter', 'Gruppenführer', 'Maschinist', 'Atemschutz', 'Sicherheitstrupp', 'Melder', 'Wassertrupp', 'Angriffstrupp', 'Mannschaft', 'Sonstiges', ] as const; export type EinsatzFunktion = (typeof EINSATZ_FUNKTIONEN)[number]; export const ALARMIERUNG_ARTEN = [ 'ILS', 'DME', 'Telefon', 'Vor_Ort', 'Sonstiges', ] as const; export type AlarmierungArt = (typeof ALARMIERUNG_ARTEN)[number]; export const ALARMIERUNG_ART_LABELS: Record = { ILS: 'ILS (Integrierte Leitstelle)', DME: 'Digitaler Meldeempfänger', Telefon: 'Telefon', Vor_Ort: 'Vor Ort', Sonstiges: 'Sonstiges', }; // --------------------------------------------------------------------------- // DATABASE ROW INTERFACES // --------------------------------------------------------------------------- /** Raw database row for einsaetze table */ export interface Einsatz { id: string; einsatz_nr: string; alarm_time: Date; ausrueck_time: Date | null; ankunft_time: Date | null; einrueck_time: Date | null; einsatz_art: EinsatzArt; einsatz_stichwort: string | null; strasse: string | null; hausnummer: string | null; ort: string | null; koordinaten: { x: number; y: number } | null; // pg returns POINT as {x,y} bericht_kurz: string | null; bericht_text: string | null; einsatzleiter_id: string | null; alarmierung_art: AlarmierungArt; status: EinsatzStatus; created_by: string | null; created_at: Date; updated_at: Date; } /** Assigned vehicle row */ export interface EinsatzFahrzeug { einsatz_id: string; fahrzeug_id: string; ausrueck_time: Date | null; einrueck_time: Date | null; assigned_at: Date; // Joined fields — aliased to 'kennzeichen' and 'fahrzeug_typ' in SQL query // to match the aliased SELECT in incident.service.ts kennzeichen?: string | null; // aliased from amtliches_kennzeichen bezeichnung?: string; fahrzeug_typ?: string | null; // aliased from typ_schluessel } /** Assigned personnel row */ export interface EinsatzPersonal { einsatz_id: string; user_id: string; funktion: EinsatzFunktion; alarm_time: Date | null; ankunft_time: Date | null; assigned_at: Date; // Joined fields name?: string | null; email?: string; given_name?: string | null; family_name?: string | null; } // --------------------------------------------------------------------------- // EXTENDED TYPES (joins / computed) // --------------------------------------------------------------------------- /** Full incident with all related data — used by detail endpoint */ export interface EinsatzWithDetails extends Einsatz { einsatzleiter_name: string | null; fahrzeuge: EinsatzFahrzeug[]; personal: EinsatzPersonal[]; /** Hilfsfrist in minutes (alarm → arrival), null if ankunft_time not set */ hilfsfrist_min: number | null; /** Total duration in minutes (alarm → return), null if einrueck_time not set */ dauer_min: number | null; } /** Lightweight row for the list view DataGrid */ export interface EinsatzListItem { id: string; einsatz_nr: string; alarm_time: Date; einsatz_art: EinsatzArt; einsatz_stichwort: string | null; ort: string | null; strasse: string | null; status: EinsatzStatus; einsatzleiter_name: string | null; /** Hilfsfrist in minutes, null if ankunft_time not set */ hilfsfrist_min: number | null; /** Total duration in minutes, null if einrueck_time not set */ dauer_min: number | null; personal_count: number; } // --------------------------------------------------------------------------- // STATISTICS TYPES // --------------------------------------------------------------------------- export interface MonthlyStatRow { monat: number; // 1–12 anzahl: number; avg_hilfsfrist_min: number | null; avg_dauer_min: number | null; } export interface EinsatzArtStatRow { einsatz_art: EinsatzArt; anzahl: number; avg_hilfsfrist_min: number | null; } export interface EinsatzStats { jahr: number; gesamt: number; abgeschlossen: number; aktiv: number; avg_hilfsfrist_min: number | null; /** Einsatzart with the highest count */ haeufigste_art: EinsatzArt | null; monthly: MonthlyStatRow[]; by_art: EinsatzArtStatRow[]; /** Previous year monthly for chart overlay */ prev_year_monthly: MonthlyStatRow[]; } // --------------------------------------------------------------------------- // ZOD VALIDATION SCHEMAS (Zod v4) // --------------------------------------------------------------------------- export const CreateEinsatzSchema = z.object({ alarm_time: z.string().datetime({ offset: true }), ausrueck_time: z.string().datetime({ offset: true }).optional().nullable(), ankunft_time: z.string().datetime({ offset: true }).optional().nullable(), einrueck_time: z.string().datetime({ offset: true }).optional().nullable(), einsatz_art: z.enum(EINSATZ_ARTEN), einsatz_stichwort: z.string().max(30).optional().nullable(), strasse: z.string().max(150).optional().nullable(), hausnummer: z.string().max(20).optional().nullable(), ort: z.string().max(100).optional().nullable(), bericht_kurz: z.string().max(255).optional().nullable(), bericht_text: z.string().optional().nullable(), einsatzleiter_id: z.string().uuid().optional().nullable(), alarmierung_art: z.enum(ALARMIERUNG_ARTEN).optional().default('ILS'), status: z.enum(EINSATZ_STATUS).optional().default('aktiv'), }); export type CreateEinsatzData = z.infer; export const UpdateEinsatzSchema = CreateEinsatzSchema.partial().omit({ alarm_time: true, // alarm_time can be updated but is handled explicitly }).extend({ // alarm_time is allowed to be updated but must remain valid if provided alarm_time: z.string().datetime({ offset: true }).optional(), }); export type UpdateEinsatzData = z.infer; export const AssignPersonnelSchema = z.object({ user_id: z.string().uuid(), funktion: z.enum(EINSATZ_FUNKTIONEN).optional().default('Mannschaft'), alarm_time: z.string().datetime({ offset: true }).optional().nullable(), ankunft_time: z.string().datetime({ offset: true }).optional().nullable(), }); export type AssignPersonnelData = z.infer; export const AssignVehicleSchema = z.object({ fahrzeug_id: z.string().uuid(), ausrueck_time: z.string().datetime({ offset: true }).optional().nullable(), einrueck_time: z.string().datetime({ offset: true }).optional().nullable(), }); export type AssignVehicleData = z.infer; export const IncidentFiltersSchema = z.object({ dateFrom: z.string().datetime({ offset: true }).optional(), dateTo: z.string().datetime({ offset: true }).optional(), einsatzArt: z.enum(EINSATZ_ARTEN).optional(), status: z.enum(EINSATZ_STATUS).optional(), limit: z.coerce.number().int().min(1).max(200).optional().default(50), offset: z.coerce.number().int().min(0).optional().default(0), }); export type IncidentFilters = z.infer;