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

View File

@@ -0,0 +1,230 @@
import { z } from 'zod';
// ============================================================
// Domain enumerations — used both as Zod schemas and runtime
// arrays (for building <Select> options in the frontend).
// ============================================================
export const DIENSTGRAD_VALUES = [
'Feuerwehranwärter',
'Feuerwehrmann',
'Feuerwehrfrau',
'Oberfeuerwehrmann',
'Oberfeuerwehrfrau',
'Hauptfeuerwehrmann',
'Hauptfeuerwehrfrau',
'Löschmeister',
'Oberlöschmeister',
'Hauptlöschmeister',
'Brandmeister',
'Oberbrandmeister',
'Hauptbrandmeister',
'Brandinspektor',
'Oberbrandinspektor',
'Brandoberinspektor',
'Brandamtmann',
] as const;
export const STATUS_VALUES = [
'aktiv',
'passiv',
'ehrenmitglied',
'jugendfeuerwehr',
'anwärter',
'ausgetreten',
] as const;
export const FUNKTION_VALUES = [
'Kommandant',
'Stellv. Kommandant',
'Gruppenführer',
'Truppführer',
'Gerätewart',
'Kassier',
'Schriftführer',
'Atemschutzwart',
'Ausbildungsbeauftragter',
] as const;
export const TSHIRT_GROESSE_VALUES = [
'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL',
] as const;
export type DienstgradEnum = typeof DIENSTGRAD_VALUES[number];
export type StatusEnum = typeof STATUS_VALUES[number];
export type FunktionEnum = typeof FUNKTION_VALUES[number];
export type TshirtGroesseEnum = typeof TSHIRT_GROESSE_VALUES[number];
// ============================================================
// Core DB row interface — mirrors the mitglieder_profile table
// exactly (snake_case, nullable columns use `| null`)
// ============================================================
export interface MitgliederProfile {
id: string;
user_id: string;
mitglieds_nr: string | null;
dienstgrad: DienstgradEnum | null;
dienstgrad_seit: Date | null;
funktion: FunktionEnum[];
status: StatusEnum;
eintrittsdatum: Date | null;
austrittsdatum: Date | null;
geburtsdatum: Date | null; // sensitive field
telefon_mobil: string | null;
telefon_privat: string | null;
notfallkontakt_name: string | null; // sensitive field
notfallkontakt_telefon: string | null; // sensitive field
fuehrerscheinklassen: string[];
tshirt_groesse: TshirtGroesseEnum | null;
schuhgroesse: string | null;
bemerkungen: string | null;
bild_url: string | null;
created_at: Date;
updated_at: Date;
}
// ============================================================
// Rank-change history row
// ============================================================
export interface DienstgradVerlaufEntry {
id: string;
user_id: string;
dienstgrad_neu: string;
dienstgrad_alt: string | null;
datum: Date;
durch_user_id: string | null;
bemerkung: string | null;
created_at: Date;
}
// ============================================================
// Joined view: users row + mitglieder_profile (nullable)
// This is what the service returns for GET /members/:userId
// ============================================================
export interface MemberWithProfile {
// --- from users ---
id: string;
email: string;
name: string | null;
given_name: string | null;
family_name: string | null;
preferred_username: string | null;
profile_picture_url: string | null;
is_active: boolean;
last_login_at: Date | null;
created_at: Date;
// --- from mitglieder_profile (null when no profile exists) ---
profile: MitgliederProfile | null;
// --- rank history (populated on detail requests) ---
dienstgrad_verlauf?: DienstgradVerlaufEntry[];
}
// ============================================================
// List-view projection — only the fields needed for the table
// ============================================================
export interface MemberListItem {
// user fields
id: string;
name: string | null;
given_name: string | null;
family_name: string | null;
email: string;
profile_picture_url: string | null;
is_active: boolean;
// profile fields (null when no profile exists)
profile_id: string | null;
mitglieds_nr: string | null;
dienstgrad: DienstgradEnum | null;
funktion: FunktionEnum[];
status: StatusEnum | null;
eintrittsdatum: Date | null;
telefon_mobil: string | null;
}
// ============================================================
// Filter parameters for getAllMembers()
// ============================================================
export interface MemberFilters {
search?: string; // matches name, email, mitglieds_nr
status?: StatusEnum[];
dienstgrad?: DienstgradEnum[];
page?: number; // 1-based
pageSize?: number;
}
// ============================================================
// Zod validation schemas
// ============================================================
/**
* Schema for POST /members/:userId/profile
* All fields are optional at creation except user_id (in URL).
* status has a default; every other field may be omitted.
*/
export const CreateMemberProfileSchema = z.object({
mitglieds_nr: z.string().max(32).optional(),
dienstgrad: z.enum(DIENSTGRAD_VALUES).optional(),
dienstgrad_seit: z.coerce.date().optional(),
funktion: z.array(z.enum(FUNKTION_VALUES)).default([]),
status: z.enum(STATUS_VALUES).default('aktiv'),
eintrittsdatum: z.coerce.date().optional(),
austrittsdatum: z.coerce.date().optional(),
geburtsdatum: z.coerce.date().optional(),
telefon_mobil: z.string().max(32).optional(),
telefon_privat: z.string().max(32).optional(),
notfallkontakt_name: z.string().max(255).optional(),
notfallkontakt_telefon: z.string().max(32).optional(),
fuehrerscheinklassen: z.array(z.string().max(8)).default([]),
tshirt_groesse: z.enum(TSHIRT_GROESSE_VALUES).optional(),
schuhgroesse: z.string().max(8).optional(),
bemerkungen: z.string().optional(),
bild_url: z.string().url().optional(),
});
export type CreateMemberProfileData = z.infer<typeof CreateMemberProfileSchema>;
/**
* Schema for PATCH /members/:userId
* Every field is optional — only provided fields are written.
*/
export const UpdateMemberProfileSchema = CreateMemberProfileSchema.partial();
export type UpdateMemberProfileData = z.infer<typeof UpdateMemberProfileSchema>;
/**
* Schema for the "self-edit" subset available to the Mitglied role.
* Only non-sensitive, personally-owned fields.
*/
export const SelfUpdateMemberProfileSchema = z.object({
telefon_mobil: z.string().max(32).optional(),
telefon_privat: z.string().max(32).optional(),
notfallkontakt_name: z.string().max(255).optional(),
notfallkontakt_telefon: z.string().max(32).optional(),
tshirt_groesse: z.enum(TSHIRT_GROESSE_VALUES).optional(),
schuhgroesse: z.string().max(8).optional(),
bild_url: z.string().url().optional(),
});
export type SelfUpdateMemberProfileData = z.infer<typeof SelfUpdateMemberProfileSchema>;
// ============================================================
// Stats response shape (for dashboard KPI endpoint)
// ============================================================
export interface MemberStats {
total: number;
aktiv: number;
passiv: number;
ehrenmitglied: number;
jugendfeuerwehr: number;
anwärter: number;
ausgetreten: number;
}

View File

@@ -0,0 +1,197 @@
import { z } from 'zod';
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
export const UEBUNG_TYPEN = [
'Übungsabend',
'Lehrgang',
'Sonderdienst',
'Versammlung',
'Gemeinschaftsübung',
'Sonstiges',
] as const;
export type UebungTyp = (typeof UEBUNG_TYPEN)[number];
export const TEILNAHME_STATUSES = [
'zugesagt',
'abgesagt',
'erschienen',
'entschuldigt',
'unbekannt',
] as const;
export type TeilnahmeStatus = (typeof TEILNAHME_STATUSES)[number];
// ---------------------------------------------------------------------------
// Core DB-mapped interfaces
// ---------------------------------------------------------------------------
export interface Uebung {
id: string;
titel: string;
beschreibung?: string | null;
typ: UebungTyp;
datum_von: Date;
datum_bis: Date;
ort?: string | null;
treffpunkt?: string | null;
pflichtveranstaltung: boolean;
mindest_teilnehmer?: number | null;
max_teilnehmer?: number | null;
angelegt_von?: string | null;
erstellt_am: Date;
aktualisiert_am: Date;
abgesagt: boolean;
absage_grund?: string | null;
}
export interface Teilnahme {
uebung_id: string;
user_id: string;
status: TeilnahmeStatus;
antwort_am?: Date | null;
erschienen_erfasst_am?: Date | null;
erschienen_erfasst_von?: string | null;
bemerkung?: string | null;
// Joined from users table
user_name?: string | null;
user_email?: string | null;
}
// ---------------------------------------------------------------------------
// Enriched / view-based interfaces
// ---------------------------------------------------------------------------
export interface AttendanceCounts {
gesamt_eingeladen: number;
anzahl_zugesagt: number;
anzahl_abgesagt: number;
anzahl_erschienen: number;
anzahl_entschuldigt: number;
anzahl_unbekannt: number;
}
/** Full event object including all attendance data — used in detail page */
export interface UebungWithAttendance extends Uebung, AttendanceCounts {
/** Only populated for users with training:write or own role >= Gruppenführer */
teilnahmen?: Teilnahme[];
/** The requesting user's own RSVP status, always included */
eigener_status?: TeilnahmeStatus;
/** Name of the person who created the event */
angelegt_von_name?: string | null;
}
/** Lightweight list item — used in calendar, upcoming list widget */
export interface UebungListItem {
id: string;
titel: string;
typ: UebungTyp;
datum_von: Date;
datum_bis: Date;
ort?: string | null;
pflichtveranstaltung: boolean;
abgesagt: boolean;
anzahl_zugesagt: number;
anzahl_erschienen: number;
gesamt_eingeladen: number;
/** Requesting user's own status — undefined when called without userId */
eigener_status?: TeilnahmeStatus;
}
// ---------------------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------------------
/** Monthly training summary for dashboard stats card */
export interface TrainingStats {
/** e.g. "2026-02" */
month: string;
/** Total events that month */
total: number;
/** Pflichtveranstaltungen that month */
pflicht: number;
/** % of events (Übungsabende only) a given user attended — 0-100 */
attendanceRate: number;
}
/** Per-member annual participation stats for Jahresbericht (Tier 3) */
export interface MemberParticipationStats {
userId: string;
name: string;
totalUebungen: number;
attended: number;
attendancePercent: number;
pflichtGesamt: number;
pflichtErschienen: number;
uebungsabendQuotePct: number;
}
// ---------------------------------------------------------------------------
// Zod validation schemas — used in service layer and route middleware
// ---------------------------------------------------------------------------
export const CreateUebungSchema = z.object({
titel: z
.string()
.min(3, 'Titel muss mindestens 3 Zeichen haben')
.max(255),
beschreibung: z.string().max(5000).optional().nullable(),
typ: z.enum(UEBUNG_TYPEN),
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)),
ort: z.string().max(255).optional().nullable(),
treffpunkt: z.string().max(255).optional().nullable(),
pflichtveranstaltung: z.boolean().default(false),
mindest_teilnehmer: z.number().int().positive().optional().nullable(),
max_teilnehmer: z.number().int().positive().optional().nullable(),
}).refine(
(d) => d.datum_bis >= d.datum_von,
{ message: 'datum_bis muss nach datum_von liegen', path: ['datum_bis'] }
).refine(
(d) =>
d.max_teilnehmer == null ||
d.mindest_teilnehmer == null ||
d.max_teilnehmer >= d.mindest_teilnehmer,
{ message: 'max_teilnehmer muss >= mindest_teilnehmer sein', path: ['max_teilnehmer'] }
);
export type CreateUebungData = z.infer<typeof CreateUebungSchema>;
export const UpdateUebungSchema = CreateUebungSchema.partial().extend({
// All fields optional on update, but retain type narrowing
});
export type UpdateUebungData = z.infer<typeof UpdateUebungSchema>;
export const UpdateRsvpSchema = z.object({
status: z.enum(['zugesagt', 'abgesagt']),
bemerkung: z.string().max(500).optional().nullable(),
});
export type UpdateRsvpData = z.infer<typeof UpdateRsvpSchema>;
export const MarkAttendanceSchema = z.object({
userIds: z
.array(z.string().uuid())
.min(1, 'Mindestens eine Person muss ausgewählt werden'),
});
export type MarkAttendanceData = z.infer<typeof MarkAttendanceSchema>;
export const CancelEventSchema = z.object({
absage_grund: z
.string()
.min(5, 'Bitte gib einen Grund für die Absage an (min. 5 Zeichen)')
.max(1000),
});
export type CancelEventData = z.infer<typeof CancelEventSchema>;

View File

@@ -0,0 +1,269 @@
// =============================================================================
// Vehicle Fleet Management — Domain Model
// =============================================================================
// ── Enums ─────────────────────────────────────────────────────────────────────
/**
* Operational status of a vehicle.
* These values are the CHECK constraint values in the database.
*/
export enum FahrzeugStatus {
Einsatzbereit = 'einsatzbereit',
AusserDienstWartung = 'ausser_dienst_wartung',
AusserDienstSchaden = 'ausser_dienst_schaden',
InLehrgang = 'in_lehrgang',
}
/** Human-readable German labels for each status value */
export const FahrzeugStatusLabel: Record<FahrzeugStatus, string> = {
[FahrzeugStatus.Einsatzbereit]: 'Einsatzbereit',
[FahrzeugStatus.AusserDienstWartung]: 'Außer Dienst (Wartung)',
[FahrzeugStatus.AusserDienstSchaden]: 'Außer Dienst (Schaden)',
[FahrzeugStatus.InLehrgang]: 'In Lehrgang',
};
/**
* Types of vehicle inspections (Prüfungsarten).
* These values are the CHECK constraint values in the database.
*/
export enum PruefungArt {
HU = 'HU', // Hauptuntersuchung (TÜV) — 24-month interval
AU = 'AU', // Abgasuntersuchung — 12-month interval
UVV = 'UVV', // Unfallverhütungsvorschrift BGV D29 — 12-month
Leiter = 'Leiter', // Leiternprüfung (DLK only) — 12-month
Kran = 'Kran', // Kranprüfung — 12-month
Seilwinde = 'Seilwinde', // Seilwindenprüfung — 12-month
Sonstiges = 'Sonstiges',
}
/** Human-readable German labels for each PruefungArt */
export const PruefungArtLabel: Record<PruefungArt, string> = {
[PruefungArt.HU]: 'Hauptuntersuchung (TÜV)',
[PruefungArt.AU]: 'Abgasuntersuchung',
[PruefungArt.UVV]: 'UVV-Prüfung (BGV D29)',
[PruefungArt.Leiter]: 'Leiternprüfung (DLK)',
[PruefungArt.Kran]: 'Kranprüfung',
[PruefungArt.Seilwinde]: 'Seilwindenprüfung',
[PruefungArt.Sonstiges]: 'Sonstige Prüfung',
};
/**
* Standard inspection intervals in months, keyed by PruefungArt.
* Used by vehicle.service.ts to auto-calculate naechste_faelligkeit.
*/
export const PruefungIntervalMonths: Partial<Record<PruefungArt, number>> = {
[PruefungArt.HU]: 24,
[PruefungArt.AU]: 12,
[PruefungArt.UVV]: 12,
[PruefungArt.Leiter]: 12,
[PruefungArt.Kran]: 12,
[PruefungArt.Seilwinde]: 12,
// Sonstiges: no standard interval — must be set manually
};
/** Inspection result values */
export type PruefungErgebnis =
| 'bestanden'
| 'bestanden_mit_maengeln'
| 'nicht_bestanden'
| 'ausstehend';
/** Maintenance log entry types */
export type WartungslogArt =
| 'Inspektion'
| 'Reparatur'
| 'Kraftstoff'
| 'Reifenwechsel'
| 'Hauptuntersuchung'
| 'Reinigung'
| 'Sonstiges';
// ── Core Entities ─────────────────────────────────────────────────────────────
/** Raw database row from the `fahrzeuge` table */
export interface Fahrzeug {
id: string; // UUID
bezeichnung: string; // e.g. "LF 20/16"
kurzname: string | null;
amtliches_kennzeichen: string | null;
fahrgestellnummer: string | null;
baujahr: number | null;
hersteller: string | null;
typ_schluessel: string | null;
besatzung_soll: string | null; // e.g. "1/8"
status: FahrzeugStatus;
status_bemerkung: string | null;
standort: string;
bild_url: string | null;
created_at: Date;
updated_at: Date;
}
/** Raw database row from `fahrzeug_pruefungen` */
export interface FahrzeugPruefung {
id: string; // UUID
fahrzeug_id: string; // UUID FK
pruefung_art: PruefungArt;
faellig_am: Date; // The hard legal deadline
durchgefuehrt_am: Date | null;
ergebnis: PruefungErgebnis | null;
naechste_faelligkeit: Date | null;
pruefende_stelle: string | null;
kosten: number | null;
dokument_url: string | null;
bemerkung: string | null;
erfasst_von: string | null; // UUID FK users
created_at: Date;
}
/** Raw database row from `fahrzeug_wartungslog` */
export interface FahrzeugWartungslog {
id: string; // UUID
fahrzeug_id: string; // UUID FK
datum: Date;
art: WartungslogArt | null;
beschreibung: string;
km_stand: number | null;
kraftstoff_liter: number | null;
kosten: number | null;
externe_werkstatt: string | null;
erfasst_von: string | null; // UUID FK users
created_at: Date;
}
// ── Inspection Status per Type ────────────────────────────────────────────────
/** Status of a single inspection type for a vehicle */
export interface PruefungStatus {
pruefung_id: string | null;
faellig_am: Date | null;
tage_bis_faelligkeit: number | null; // negative = overdue
ergebnis: PruefungErgebnis | null;
}
/**
* Vehicle with its per-type inspection status.
* Comes from the `fahrzeuge_mit_pruefstatus` view.
*/
export interface FahrzeugWithPruefstatus extends Fahrzeug {
pruefstatus: {
hu: PruefungStatus;
au: PruefungStatus;
uvv: PruefungStatus;
leiter: PruefungStatus;
};
/** Minimum tage_bis_faelligkeit across all inspections (negative = any overdue) */
naechste_pruefung_tage: number | null;
/** Full inspection history, ordered by faellig_am DESC */
pruefungen: FahrzeugPruefung[];
/** Maintenance log entries, ordered by datum DESC */
wartungslog: FahrzeugWartungslog[];
}
// ── List Item (Grid / Card view) ──────────────────────────────────────────────
/**
* Lightweight type used in the vehicle fleet overview grid.
* Includes only the fields needed to render a card plus inspection badges.
*/
export interface FahrzeugListItem {
id: string;
bezeichnung: string;
kurzname: string | null;
amtliches_kennzeichen: string | null;
baujahr: number | null;
hersteller: string | null;
besatzung_soll: string | null;
status: FahrzeugStatus;
status_bemerkung: string | null;
bild_url: string | null;
hu_faellig_am: Date | null;
hu_tage_bis_faelligkeit: number | null;
au_faellig_am: Date | null;
au_tage_bis_faelligkeit: number | null;
uvv_faellig_am: Date | null;
uvv_tage_bis_faelligkeit: number | null;
leiter_faellig_am: Date | null;
leiter_tage_bis_faelligkeit: number | null;
naechste_pruefung_tage: number | null;
}
// ── Dashboard KPI ─────────────────────────────────────────────────────────────
/** Aggregated vehicle stats for the dashboard KPI strip */
export interface VehicleStats {
total: number;
einsatzbereit: number;
ausserDienst: number; // wartung + schaden combined
inLehrgang: number;
inspectionsDue: number; // vehicles with any inspection due within 30 days
inspectionsOverdue: number; // vehicles with any inspection already overdue
}
// ── Inspection Alert ──────────────────────────────────────────────────────────
/** Single alert item for the dashboard InspectionAlerts component */
export interface InspectionAlert {
fahrzeugId: string;
bezeichnung: string;
kurzname: string | null;
pruefungId: string;
pruefungArt: PruefungArt;
faelligAm: Date;
tage: number; // negative = already overdue
}
// ── Create / Update DTOs ──────────────────────────────────────────────────────
export interface CreateFahrzeugData {
bezeichnung: string;
kurzname?: string;
amtliches_kennzeichen?: string;
fahrgestellnummer?: string;
baujahr?: number;
hersteller?: string;
typ_schluessel?: string;
besatzung_soll?: string;
status?: FahrzeugStatus;
status_bemerkung?: string;
standort?: string;
bild_url?: string;
}
export interface UpdateFahrzeugData {
bezeichnung?: string;
kurzname?: string | null;
amtliches_kennzeichen?: string | null;
fahrgestellnummer?: string | null;
baujahr?: number | null;
hersteller?: string | null;
typ_schluessel?: string | null;
besatzung_soll?: string | null;
status?: FahrzeugStatus;
status_bemerkung?: string | null;
standort?: string;
bild_url?: string | null;
}
export interface CreatePruefungData {
pruefung_art: PruefungArt;
faellig_am: string; // ISO date string 'YYYY-MM-DD'
durchgefuehrt_am?: string; // ISO date string, optional
ergebnis?: PruefungErgebnis;
pruefende_stelle?: string;
kosten?: number;
dokument_url?: string;
bemerkung?: string;
// naechste_faelligkeit is auto-calculated by the service — not accepted from client
}
export interface CreateWartungslogData {
datum: string; // ISO date string 'YYYY-MM-DD'
art?: WartungslogArt;
beschreibung: string;
km_stand?: number;
kraftstoff_liter?: number;
kosten?: number;
externe_werkstatt?: string;
}