From 260b71baf86e3ab933ece360ee35f976a3ee335d Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Wed, 15 Apr 2026 19:43:18 +0200 Subject: [PATCH] =?UTF-8?q?refactor(mitglieder):=20replace=20legacy=20stat?= =?UTF-8?q?us=20values=20(passiv/anw=C3=A4rter/ausgetreten/=E2=80=A6)=20wi?= =?UTF-8?q?th=20aktiv/kind/jugend/reserve=20across=20backend,=20frontend,?= =?UTF-8?q?=20and=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/member.controller.ts | 2 +- .../migrations/090_update_status_values.sql | 16 ++++++++++ backend/src/models/member.model.ts | 16 ++++------ backend/src/services/atemschutz.service.ts | 4 +-- backend/src/services/member.service.ts | 20 +++++------- backend/src/types/multer.d.ts | 32 +++++++++---------- frontend/src/pages/Buchhaltung.tsx | 1 - frontend/src/services/members.ts | 2 +- frontend/src/types/member.types.ts | 32 +++++++------------ sync/src/scraper.ts | 22 +++++++++++-- sync/src/types.ts | 4 +-- 11 files changed, 84 insertions(+), 67 deletions(-) create mode 100644 backend/src/database/migrations/090_update_status_values.sql diff --git a/backend/src/controllers/member.controller.ts b/backend/src/controllers/member.controller.ts index 412f610..188bfc6 100644 --- a/backend/src/controllers/member.controller.ts +++ b/backend/src/controllers/member.controller.ts @@ -47,7 +47,7 @@ class MemberController { pageSize, } = req.query as Record; - // Arrays can be sent as ?status[]=aktiv&status[]=passiv or CSV + // Arrays can be sent as ?status[]=aktiv&status[]=jugend or CSV const statusParam = req.query['status'] as string | string[] | undefined; const dienstgradParam = req.query['dienstgrad'] as string | string[] | undefined; diff --git a/backend/src/database/migrations/090_update_status_values.sql b/backend/src/database/migrations/090_update_status_values.sql new file mode 100644 index 0000000..1de55a7 --- /dev/null +++ b/backend/src/database/migrations/090_update_status_values.sql @@ -0,0 +1,16 @@ +-- Migration: 090_update_status_values +-- Replace old status values with FDISK-aligned values: aktiv, kind, jugend, reserve. +-- Old values passiv, ehrenmitglied, jugendfeuerwehr, anwärter, ausgetreten are removed. +-- Idempotent: safe to run multiple times. + +-- 1. Drop existing CHECK constraint +ALTER TABLE mitglieder_profile DROP CONSTRAINT IF EXISTS mitglieder_profile_status_check; + +-- 2. Migrate existing data +UPDATE mitglieder_profile SET status = 'jugend' WHERE status = 'jugendfeuerwehr'; +UPDATE mitglieder_profile SET status = NULL + WHERE status IN ('passiv', 'ehrenmitglied', 'anwärter', 'ausgetreten'); + +-- 3. Re-add CHECK with new allowed values (NULL still allowed for profiles without FDISK sync) +ALTER TABLE mitglieder_profile ADD CONSTRAINT mitglieder_profile_status_check + CHECK (status IS NULL OR status IN ('aktiv', 'kind', 'jugend', 'reserve')); diff --git a/backend/src/models/member.model.ts b/backend/src/models/member.model.ts index 6e48138..0a6b913 100644 --- a/backend/src/models/member.model.ts +++ b/backend/src/models/member.model.ts @@ -29,11 +29,9 @@ export const DIENSTGRAD_VALUES = [ export const STATUS_VALUES = [ 'aktiv', - 'passiv', - 'ehrenmitglied', - 'jugendfeuerwehr', - 'anwärter', - 'ausgetreten', + 'kind', + 'jugend', + 'reserve', ] as const; export const FUNKTION_VALUES = [ @@ -232,9 +230,7 @@ export type SelfUpdateMemberProfileData = z.infer { /** * Builds a URLSearchParams object from the filter object so query - * strings like ?status[]=aktiv&status[]=passiv are sent correctly. + * strings like ?status[]=aktiv&status[]=jugend are sent correctly. */ function buildParams(filters?: MemberFilters): URLSearchParams { const params = new URLSearchParams(); diff --git a/frontend/src/types/member.types.ts b/frontend/src/types/member.types.ts index 5dc1587..3be529a 100644 --- a/frontend/src/types/member.types.ts +++ b/frontend/src/types/member.types.ts @@ -27,11 +27,9 @@ export const DIENSTGRAD_VALUES = [ export const STATUS_VALUES = [ 'aktiv', - 'passiv', - 'ehrenmitglied', - 'jugendfeuerwehr', - 'anwärter', - 'ausgetreten', + 'kind', + 'jugend', + 'reserve', ] as const; export const FUNKTION_VALUES = [ @@ -144,11 +142,9 @@ export type UpdateMemberProfileData = CreateMemberProfileData; export interface MemberStats { total: number; aktiv: number; - passiv: number; - ehrenmitglied: number; - jugendfeuerwehr: number; - 'anwärter': number; - ausgetreten: number; + kind: number; + jugend: number; + reserve: number; } // ---------------------------------------------------------------- @@ -184,21 +180,17 @@ export function formatPhone(raw: string | null | undefined): string { /** Returns a human-readable status label */ export const STATUS_LABELS: Record = { aktiv: 'Aktiv', - passiv: 'Passiv', - ehrenmitglied: 'Ehrenmitglied', - jugendfeuerwehr: 'Jugendfeuerwehr', - anwärter: 'Anwärter', - ausgetreten: 'Ausgetreten', + kind: 'Kind', + jugend: 'Jugend', + reserve: 'Reserve', }; /** MUI Chip color for each status */ export const STATUS_COLORS: Record = { aktiv: 'success', - passiv: 'warning', - ehrenmitglied: 'info', - jugendfeuerwehr: 'info', - anwärter: 'default', - ausgetreten: 'error', + kind: 'info', + jugend: 'info', + reserve: 'warning', }; // ---------------------------------------------------------------- diff --git a/sync/src/scraper.ts b/sync/src/scraper.ts index 01b438d..07e5b3f 100644 --- a/sync/src/scraper.ts +++ b/sync/src/scraper.ts @@ -33,6 +33,20 @@ async function dumpHtml(frame: Frame, label: string): Promise { } } +/** + * Maps a raw FDISK status string to a dashboard status value. + * Returns null for unknown/unneeded statuses — those members should be skipped. + */ +function mapFdiskStatus(raw: string): 'aktiv' | 'kind' | 'jugend' | 'reserve' | null { + switch (raw.trim()) { + case 'Aktiv': return 'aktiv'; + case 'Kind': return 'kind'; + case 'Jugend': return 'jugend'; + case 'Reserve': return 'reserve'; + default: return null; + } +} + function log(msg: string) { console.log(`[scraper] ${new Date().toISOString()} ${msg}`); } @@ -153,6 +167,8 @@ async function scrapeKnownMembers( const members: FdiskMember[] = []; for (const row of allRows) { if (!row.standesbuchNr || !row.vorname || !row.zuname) continue; + const status = mapFdiskStatus(row.status); + if (!status) continue; // skip members with non-synced statuses const abmeldedatum = parseDate(row.abmeldedatum); members.push({ standesbuchNr: row.standesbuchNr, @@ -163,7 +179,7 @@ async function scrapeKnownMembers( svnr: row.svnr || null, eintrittsdatum: parseDate(row.eintrittsdatum), abmeldedatum, - status: abmeldedatum ? 'ausgetreten' : 'aktiv', + status, detailUrl: row.href, geburtsort: null, geschlecht: null, @@ -593,6 +609,8 @@ async function scrapeMembers(frame: Frame): Promise { const members: FdiskMember[] = []; for (const row of allRows) { if (!row.standesbuchNr || !row.vorname || !row.zuname) continue; + const status = mapFdiskStatus(row.status); + if (!status) continue; // skip members with non-synced statuses const abmeldedatum = parseDate(row.abmeldedatum); members.push({ standesbuchNr: row.standesbuchNr, @@ -603,7 +621,7 @@ async function scrapeMembers(frame: Frame): Promise { svnr: row.svnr || null, eintrittsdatum: parseDate(row.eintrittsdatum), abmeldedatum, - status: abmeldedatum ? 'ausgetreten' : 'aktiv', + status, detailUrl: row.href, geburtsort: null, geschlecht: null, diff --git a/sync/src/types.ts b/sync/src/types.ts index 50592f0..f968287 100644 --- a/sync/src/types.ts +++ b/sync/src/types.ts @@ -7,8 +7,8 @@ export interface FdiskMember { svnr: string | null; eintrittsdatum: string | null; abmeldedatum: string | null; - /** 'aktiv' if no Abmeldedatum, 'ausgetreten' otherwise */ - status: 'aktiv' | 'ausgetreten'; + /** Status mapped from FDISK column: Aktiv/Kind/Jugend/Reserve */ + status: 'aktiv' | 'kind' | 'jugend' | 'reserve'; /** URL or identifier to navigate to the member detail page */ detailUrl: string | null; /** Additional profile fields scraped from the detail form */