add features
This commit is contained in:
230
backend/src/models/member.model.ts
Normal file
230
backend/src/models/member.model.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user