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,194 @@
// ----------------------------------------------------------------
// Frontend mirror of backend/src/models/member.model.ts
// Keep in sync when the model changes.
// ----------------------------------------------------------------
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];
export interface MitgliederProfile {
id: string;
user_id: string;
mitglieds_nr: string | null;
dienstgrad: DienstgradEnum | null;
dienstgrad_seit: string | null; // ISO date string from API
funktion: FunktionEnum[];
status: StatusEnum;
eintrittsdatum: string | null;
austrittsdatum: string | null;
geburtsdatum: string | null; // null when redacted by server
_age?: number; // synthesised when geburtsdatum is redacted
telefon_mobil: string | null;
telefon_privat: string | null;
notfallkontakt_name: string | null;
notfallkontakt_telefon: string | null;
fuehrerscheinklassen: string[];
tshirt_groesse: TshirtGroesseEnum | null;
schuhgroesse: string | null;
bemerkungen: string | null;
bild_url: string | null;
created_at: string;
updated_at: string;
}
export interface DienstgradVerlaufEntry {
id: string;
user_id: string;
dienstgrad_neu: string;
dienstgrad_alt: string | null;
datum: string;
durch_user_id: string | null;
durch_user_name?: string | null;
bemerkung: string | null;
created_at: string;
}
export interface MemberWithProfile {
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: string | null;
created_at: string;
profile: MitgliederProfile | null;
dienstgrad_verlauf?: DienstgradVerlaufEntry[];
}
export interface MemberListItem {
id: string;
name: string | null;
given_name: string | null;
family_name: string | null;
email: string;
profile_picture_url: string | null;
is_active: boolean;
profile_id: string | null;
mitglieds_nr: string | null;
dienstgrad: DienstgradEnum | null;
funktion: FunktionEnum[];
status: StatusEnum | null;
eintrittsdatum: string | null;
telefon_mobil: string | null;
}
export interface MemberFilters {
search?: string;
status?: StatusEnum[];
dienstgrad?: DienstgradEnum[];
page?: number;
pageSize?: number;
}
export type CreateMemberProfileData = Partial<Omit<MitgliederProfile, 'id' | 'user_id' | 'created_at' | 'updated_at'>>;
export type UpdateMemberProfileData = CreateMemberProfileData;
export interface MemberStats {
total: number;
aktiv: number;
passiv: number;
ehrenmitglied: number;
jugendfeuerwehr: number;
'anwärter': number;
ausgetreten: number;
}
// ----------------------------------------------------------------
// Display helpers
// ----------------------------------------------------------------
/** Returns the display name built from given/family name or email fallback */
export function getMemberDisplayName(
member: Pick<MemberWithProfile | MemberListItem, 'given_name' | 'family_name' | 'name' | 'email'>
): string {
if (member.given_name || member.family_name) {
return [member.given_name, member.family_name].filter(Boolean).join(' ');
}
return member.name || member.email;
}
/** Format a German phone number for display.
* Stored raw; displayed with spaces for readability.
* e.g. "+436641234567" → "+43 664 123 4567"
* Falls back to raw value for unrecognised formats. */
export function formatPhone(raw: string | null | undefined): string {
if (!raw) return '—';
const digits = raw.replace(/\s/g, '');
// Austrian mobile: +43 6xx xxx xxxx
const atMobile = digits.match(/^(\+43)(6\d{2})(\d{3,4})(\d{4})$/);
if (atMobile) return `${atMobile[1]} ${atMobile[2]} ${atMobile[3]} ${atMobile[4]}`;
// German mobile: +49 1xx xxx xxxxx
const deMobile = digits.match(/^(\+49)(1\d{2})(\d{3,4})(\d{4,5})$/);
if (deMobile) return `${deMobile[1]} ${deMobile[2]} ${deMobile[3]} ${deMobile[4]}`;
return raw;
}
/** Returns a human-readable status label */
export const STATUS_LABELS: Record<StatusEnum, string> = {
aktiv: 'Aktiv',
passiv: 'Passiv',
ehrenmitglied: 'Ehrenmitglied',
jugendfeuerwehr: 'Jugendfeuerwehr',
anwärter: 'Anwärter',
ausgetreten: 'Ausgetreten',
};
/** MUI Chip color for each status */
export const STATUS_COLORS: Record<StatusEnum, 'success' | 'warning' | 'error' | 'info' | 'default'> = {
aktiv: 'success',
passiv: 'warning',
ehrenmitglied: 'info',
jugendfeuerwehr: 'info',
anwärter: 'default',
ausgetreten: 'error',
};

View File

@@ -0,0 +1,115 @@
// ---------------------------------------------------------------------------
// Frontend training types — mirrors backend/src/models/training.model.ts
// ---------------------------------------------------------------------------
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];
export interface Uebung {
id: string;
titel: string;
beschreibung?: string | null;
typ: UebungTyp;
datum_von: string; // ISO string from JSON
datum_bis: string;
ort?: string | null;
treffpunkt?: string | null;
pflichtveranstaltung: boolean;
mindest_teilnehmer?: number | null;
max_teilnehmer?: number | null;
angelegt_von?: string | null;
erstellt_am: string;
aktualisiert_am: string;
abgesagt: boolean;
absage_grund?: string | null;
}
export interface Teilnahme {
uebung_id: string;
user_id: string;
status: TeilnahmeStatus;
antwort_am?: string | null;
erschienen_erfasst_am?: string | null;
erschienen_erfasst_von?: string | null;
bemerkung?: string | null;
user_name?: string | null;
user_email?: string | null;
}
export interface AttendanceCounts {
gesamt_eingeladen: number;
anzahl_zugesagt: number;
anzahl_abgesagt: number;
anzahl_erschienen: number;
anzahl_entschuldigt: number;
anzahl_unbekannt: number;
}
export interface UebungWithAttendance extends Uebung, AttendanceCounts {
teilnahmen?: Teilnahme[];
eigener_status?: TeilnahmeStatus;
angelegt_von_name?: string | null;
}
export interface UebungListItem {
id: string;
titel: string;
typ: UebungTyp;
datum_von: string;
datum_bis: string;
ort?: string | null;
pflichtveranstaltung: boolean;
abgesagt: boolean;
anzahl_zugesagt: number;
anzahl_erschienen: number;
gesamt_eingeladen: number;
eigener_status?: TeilnahmeStatus;
}
export interface MemberParticipationStats {
userId: string;
name: string;
totalUebungen: number;
attended: number;
attendancePercent: number;
pflichtGesamt: number;
pflichtErschienen: number;
uebungsabendQuotePct: number;
}
// ---------------------------------------------------------------------------
// Form data types (sent to API)
// ---------------------------------------------------------------------------
export interface CreateUebungData {
titel: string;
beschreibung?: string | null;
typ: UebungTyp;
datum_von: string; // ISO-8601 with offset
datum_bis: string;
ort?: string | null;
treffpunkt?: string | null;
pflichtveranstaltung: boolean;
mindest_teilnehmer?: number | null;
max_teilnehmer?: number | null;
}
export type UpdateUebungData = Partial<CreateUebungData>;

View File

@@ -0,0 +1,205 @@
// =============================================================================
// Vehicle Fleet Management — Frontend Type Definitions
// Mirror of backend/src/models/vehicle.model.ts (transport layer shapes)
// =============================================================================
export enum FahrzeugStatus {
Einsatzbereit = 'einsatzbereit',
AusserDienstWartung = 'ausser_dienst_wartung',
AusserDienstSchaden = 'ausser_dienst_schaden',
InLehrgang = 'in_lehrgang',
}
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',
};
export enum PruefungArt {
HU = 'HU',
AU = 'AU',
UVV = 'UVV',
Leiter = 'Leiter',
Kran = 'Kran',
Seilwinde = 'Seilwinde',
Sonstiges = 'Sonstiges',
}
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',
};
export type PruefungErgebnis =
| 'bestanden'
| 'bestanden_mit_maengeln'
| 'nicht_bestanden'
| 'ausstehend';
export type WartungslogArt =
| 'Inspektion'
| 'Reparatur'
| 'Kraftstoff'
| 'Reifenwechsel'
| 'Hauptuntersuchung'
| 'Reinigung'
| 'Sonstiges';
// ── API Response Shapes ───────────────────────────────────────────────────────
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: string | null; // ISO date string from API
hu_tage_bis_faelligkeit: number | null;
au_faellig_am: string | null;
au_tage_bis_faelligkeit: number | null;
uvv_faellig_am: string | null;
uvv_tage_bis_faelligkeit: number | null;
leiter_faellig_am: string | null;
leiter_tage_bis_faelligkeit: number | null;
naechste_pruefung_tage: number | null;
}
export interface PruefungStatus {
pruefung_id: string | null;
faellig_am: string | null;
tage_bis_faelligkeit: number | null;
ergebnis: PruefungErgebnis | null;
}
export interface FahrzeugPruefung {
id: string;
fahrzeug_id: string;
pruefung_art: PruefungArt;
faellig_am: string;
durchgefuehrt_am: string | null;
ergebnis: PruefungErgebnis | null;
naechste_faelligkeit: string | null;
pruefende_stelle: string | null;
kosten: number | null;
dokument_url: string | null;
bemerkung: string | null;
erfasst_von: string | null;
created_at: string;
}
export interface FahrzeugWartungslog {
id: string;
fahrzeug_id: string;
datum: string;
art: WartungslogArt | null;
beschreibung: string;
km_stand: number | null;
kraftstoff_liter: number | null;
kosten: number | null;
externe_werkstatt: string | null;
erfasst_von: string | null;
created_at: string;
}
export interface FahrzeugDetail {
id: string;
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;
created_at: string;
updated_at: string;
pruefstatus: {
hu: PruefungStatus;
au: PruefungStatus;
uvv: PruefungStatus;
leiter: PruefungStatus;
};
naechste_pruefung_tage: number | null;
pruefungen: FahrzeugPruefung[];
wartungslog: FahrzeugWartungslog[];
}
export interface VehicleStats {
total: number;
einsatzbereit: number;
ausserDienst: number;
inLehrgang: number;
inspectionsDue: number;
inspectionsOverdue: number;
}
export interface InspectionAlert {
fahrzeugId: string;
bezeichnung: string;
kurzname: string | null;
pruefungId: string;
pruefungArt: PruefungArt;
faelligAm: string;
tage: number;
}
// ── Request Payload Types ─────────────────────────────────────────────────────
export interface CreateFahrzeugPayload {
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 type UpdateFahrzeugPayload = Partial<CreateFahrzeugPayload>;
export interface UpdateStatusPayload {
status: FahrzeugStatus;
bemerkung?: string;
}
export interface CreatePruefungPayload {
pruefung_art: PruefungArt;
faellig_am: string;
durchgefuehrt_am?: string;
ergebnis?: PruefungErgebnis;
pruefende_stelle?: string;
kosten?: number;
dokument_url?: string;
bemerkung?: string;
}
export interface CreateWartungslogPayload {
datum: string;
art?: WartungslogArt;
beschreibung: string;
km_stand?: number;
kraftstoff_liter?: number;
kosten?: number;
externe_werkstatt?: string;
}