add features

This commit is contained in:
Matthias Hochmeister
2026-02-27 19:50:14 +01:00
parent c5e8337a69
commit 620bacc6b5
46 changed files with 14095 additions and 1 deletions

View File

@@ -0,0 +1,261 @@
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>;