Files
dashboard/backend/src/models/incident.model.ts
Matthias Hochmeister 620bacc6b5 add features
2026-02-27 19:50:14 +01:00

262 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<EinsatzArt, string> = {
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<EinsatzStatus, string> = {
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<AlarmierungArt, string> = {
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; // 112
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<typeof CreateEinsatzSchema>;
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<typeof UpdateEinsatzSchema>;
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<typeof AssignPersonnelSchema>;
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<typeof AssignVehicleSchema>;
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<typeof IncidentFiltersSchema>;