fix(dienstgrad): add ASB→Abschnittssachbearbeiter, remove non-existent ranks (FA/FF/BOI/BAM variants), sync DB constraint, TS types, and display map

This commit is contained in:
Matthias Hochmeister
2026-04-15 19:26:21 +02:00
parent eb2342684e
commit c1de8bd163
6 changed files with 112 additions and 48 deletions

View File

@@ -0,0 +1,66 @@
-- Migration 090: Add 'Abschnittssachbearbeiter' and remove non-existent ranks
-- (Feuerwehranwärter, Feuerwehrfrau variants, Brandoberinspektor, Brandamtmann)
-- from the mitglieder_profile dienstgrad CHECK constraint.
-- Null out any existing rows with ranks that are being removed
UPDATE mitglieder_profile
SET dienstgrad = NULL
WHERE dienstgrad IN (
'Feuerwehranwärter',
'Feuerwehrfrau',
'Oberfeuerwehrfrau',
'Hauptfeuerwehrfrau',
'Brandoberinspektor',
'Brandamtmann',
'Ehren-Feuerwehrfrau',
'Ehren-Oberfeuerwehrfrau',
'Ehren-Hauptfeuerwehrfrau',
'Ehren-Brandoberinspektor',
'Ehren-Brandamtmann'
);
ALTER TABLE mitglieder_profile
DROP CONSTRAINT IF EXISTS mitglieder_profile_dienstgrad_check;
ALTER TABLE mitglieder_profile
ADD CONSTRAINT mitglieder_profile_dienstgrad_check
CHECK (dienstgrad IS NULL OR dienstgrad IN (
-- Standard Dienstgrade
'Jugendfeuerwehrmann',
'Probefeuerwehrmann',
'Feuerwehrmann',
'Oberfeuerwehrmann',
'Hauptfeuerwehrmann',
'Löschmeister',
'Oberlöschmeister',
'Hauptlöschmeister',
'Brandmeister',
'Oberbrandmeister',
'Hauptbrandmeister',
'Brandinspektor',
'Oberbrandinspektor',
'Verwaltungsmeister',
'Oberverwaltungsmeister',
'Hauptverwaltungsmeister',
'Verwalter',
'Sachbearbeiter',
'Abschnittssachbearbeiter',
-- Ehrendienstgrade
'Ehren-Feuerwehrmann',
'Ehren-Oberfeuerwehrmann',
'Ehren-Hauptfeuerwehrmann',
'Ehren-Löschmeister',
'Ehren-Oberlöschmeister',
'Ehren-Hauptlöschmeister',
'Ehren-Brandmeister',
'Ehren-Oberbrandmeister',
'Ehren-Hauptbrandmeister',
'Ehren-Brandinspektor',
'Ehren-Oberbrandinspektor',
'Ehren-Verwaltungsmeister',
'Ehren-Oberverwaltungsmeister',
'Ehren-Hauptverwaltungsmeister',
'Ehren-Verwalter',
'Ehren-Sachbearbeiter',
'Ehren-Abschnittssachbearbeiter'
));

View File

@@ -6,13 +6,11 @@ import { z } from 'zod';
// ============================================================ // ============================================================
export const DIENSTGRAD_VALUES = [ export const DIENSTGRAD_VALUES = [
'Feuerwehranwärter', 'Jugendfeuerwehrmann',
'Probefeuerwehrmann',
'Feuerwehrmann', 'Feuerwehrmann',
'Feuerwehrfrau',
'Oberfeuerwehrmann', 'Oberfeuerwehrmann',
'Oberfeuerwehrfrau',
'Hauptfeuerwehrmann', 'Hauptfeuerwehrmann',
'Hauptfeuerwehrfrau',
'Löschmeister', 'Löschmeister',
'Oberlöschmeister', 'Oberlöschmeister',
'Hauptlöschmeister', 'Hauptlöschmeister',
@@ -21,8 +19,12 @@ export const DIENSTGRAD_VALUES = [
'Hauptbrandmeister', 'Hauptbrandmeister',
'Brandinspektor', 'Brandinspektor',
'Oberbrandinspektor', 'Oberbrandinspektor',
'Brandoberinspektor', 'Verwaltungsmeister',
'Brandamtmann', 'Oberverwaltungsmeister',
'Hauptverwaltungsmeister',
'Verwalter',
'Sachbearbeiter',
'Abschnittssachbearbeiter',
] as const; ] as const;
export const STATUS_VALUES = [ export const STATUS_VALUES = [

View File

@@ -63,25 +63,25 @@ import { ConfirmDialog, StatusChip, PageHeader } from '../components/templates';
// ── Helpers ── // ── Helpers ──
const DIENSTGRAD_KURZ: Record<string, string> = { const DIENSTGRAD_KURZ: Record<string, string> = {
'Feuerwehranwärter': 'FA', 'Jugendfeuerwehrmann': 'JFM', 'Probefeuerwehrmann': 'PFM', 'Jugendfeuerwehrmann': 'JFM', 'Probefeuerwehrmann': 'PFM',
'Feuerwehrmann': 'FM', 'Feuerwehrfrau': 'FF', 'Feuerwehrmann': 'FM',
'Oberfeuerwehrmann': 'OFM', 'Oberfeuerwehrfrau': 'OFF', 'Oberfeuerwehrmann': 'OFM',
'Hauptfeuerwehrmann': 'HFM', 'Hauptfeuerwehrfrau': 'HFF', 'Hauptfeuerwehrmann': 'HFM',
'Löschmeister': 'LM', 'Oberlöschmeister': 'OLM', 'Hauptlöschmeister': 'HLM', 'Löschmeister': 'LM', 'Oberlöschmeister': 'OLM', 'Hauptlöschmeister': 'HLM',
'Brandmeister': 'BM', 'Oberbrandmeister': 'OBM', 'Hauptbrandmeister': 'HBM', 'Brandmeister': 'BM', 'Oberbrandmeister': 'OBM', 'Hauptbrandmeister': 'HBM',
'Brandinspektor': 'BI', 'Oberbrandinspektor': 'OBI', 'Brandoberinspektor': 'BOI', 'Brandinspektor': 'BI', 'Oberbrandinspektor': 'OBI',
'Brandamtmann': 'BAM',
'Verwaltungsmeister': 'VM', 'Oberverwaltungsmeister': 'OVM', 'Verwaltungsmeister': 'VM', 'Oberverwaltungsmeister': 'OVM',
'Hauptverwaltungsmeister': 'HVM', 'Verwalter': 'V', 'Hauptverwaltungsmeister': 'HVM', 'Verwalter': 'V',
'Ehren-Feuerwehrmann': 'E-FM', 'Ehren-Feuerwehrfrau': 'E-FF', 'Sachbearbeiter': 'SB', 'Abschnittssachbearbeiter': 'ASB',
'Ehren-Oberfeuerwehrmann': 'E-OFM', 'Ehren-Oberfeuerwehrfrau': 'E-OFF', 'Ehren-Feuerwehrmann': 'E-FM',
'Ehren-Hauptfeuerwehrmann': 'E-HFM', 'Ehren-Hauptfeuerwehrfrau': 'E-HFF', 'Ehren-Oberfeuerwehrmann': 'E-OFM',
'Ehren-Hauptfeuerwehrmann': 'E-HFM',
'Ehren-Löschmeister': 'E-LM', 'Ehren-Oberlöschmeister': 'E-OLM', 'Ehren-Hauptlöschmeister': 'E-HLM', 'Ehren-Löschmeister': 'E-LM', 'Ehren-Oberlöschmeister': 'E-OLM', 'Ehren-Hauptlöschmeister': 'E-HLM',
'Ehren-Brandmeister': 'E-BM', 'Ehren-Oberbrandmeister': 'E-OBM', 'Ehren-Hauptbrandmeister': 'E-HBM', 'Ehren-Brandmeister': 'E-BM', 'Ehren-Oberbrandmeister': 'E-OBM', 'Ehren-Hauptbrandmeister': 'E-HBM',
'Ehren-Brandinspektor': 'E-BI', 'Ehren-Oberbrandinspektor': 'E-OBI', 'Ehren-Brandoberinspektor': 'E-BOI', 'Ehren-Brandinspektor': 'E-BI', 'Ehren-Oberbrandinspektor': 'E-OBI',
'Ehren-Brandamtmann': 'E-BAM',
'Ehren-Verwaltungsmeister': 'E-VM', 'Ehren-Oberverwaltungsmeister': 'E-OVM', 'Ehren-Verwaltungsmeister': 'E-VM', 'Ehren-Oberverwaltungsmeister': 'E-OVM',
'Ehren-Hauptverwaltungsmeister': 'E-HVM', 'Ehren-Verwalter': 'E-V', 'Ehren-Hauptverwaltungsmeister': 'E-HVM', 'Ehren-Verwalter': 'E-V',
'Ehren-Sachbearbeiter': 'ESB', 'Ehren-Abschnittssachbearbeiter': 'EASB',
}; };
const kurzDienstgrad = (d?: string) => (d ? (DIENSTGRAD_KURZ[d] ?? d) : undefined); const kurzDienstgrad = (d?: string) => (d ? (DIENSTGRAD_KURZ[d] ?? d) : undefined);

View File

@@ -117,7 +117,7 @@ function Mitglieder() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [debouncedSearch, selectedStatus, selectedDienstgrad, page]); }, [debouncedSearch, selectedStatus, selectedDienstgrad, page, pageSize]);
useEffect(() => { useEffect(() => {
// Reset to page 0 when search changes // Reset to page 0 when search changes
@@ -137,8 +137,7 @@ function Mitglieder() {
return; return;
} }
fetchMembers(); fetchMembers();
// eslint-disable-next-line react-hooks/exhaustive-deps }, [page, pageSize, selectedStatus, selectedDienstgrad]);
}, [page, selectedStatus, selectedDienstgrad]);
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// Event handlers // Event handlers
@@ -275,6 +274,22 @@ function Mitglieder() {
{/* Table */} {/* Table */}
<Paper sx={{ width: '100%', overflow: 'hidden' }}> <Paper sx={{ width: '100%', overflow: 'hidden' }}>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={(_e, newPage) => setPage(newPage)}
rowsPerPage={pageSize}
rowsPerPageOptions={[25, 50, 100, { value: -1, label: 'Alle' }]}
onRowsPerPageChange={(e) => {
setPageSize(parseInt(e.target.value, 10));
setPage(0);
}}
labelRowsPerPage="Einträge pro Seite:"
labelDisplayedRows={({ from, to, count }) =>
`${from}${to} von ${count !== -1 ? count : `mehr als ${to}`}`
}
/>
<DataTable<MemberListItem> <DataTable<MemberListItem>
columns={[ columns={[
{ key: 'profile_picture_url', label: 'Foto', width: 56, sortable: false, searchable: false, render: (member) => { { key: 'profile_picture_url', label: 'Foto', width: 56, sortable: false, searchable: false, render: (member) => {
@@ -330,22 +345,6 @@ function Mitglieder() {
stickyHeader stickyHeader
/> />
<TablePagination
component="div"
count={total}
page={page}
onPageChange={(_e, newPage) => setPage(newPage)}
rowsPerPage={pageSize}
rowsPerPageOptions={[25, 50, 100, { value: -1, label: 'Alle' }]}
onRowsPerPageChange={(e) => {
setPageSize(parseInt(e.target.value, 10));
setPage(0);
}}
labelRowsPerPage="Einträge pro Seite:"
labelDisplayedRows={({ from, to, count }) =>
`${from}${to} von ${count !== -1 ? count : `mehr als ${to}`}`
}
/>
</Paper> </Paper>
</Container> </Container>

View File

@@ -4,13 +4,11 @@
// ---------------------------------------------------------------- // ----------------------------------------------------------------
export const DIENSTGRAD_VALUES = [ export const DIENSTGRAD_VALUES = [
'Feuerwehranwärter', 'Jugendfeuerwehrmann',
'Probefeuerwehrmann',
'Feuerwehrmann', 'Feuerwehrmann',
'Feuerwehrfrau',
'Oberfeuerwehrmann', 'Oberfeuerwehrmann',
'Oberfeuerwehrfrau',
'Hauptfeuerwehrmann', 'Hauptfeuerwehrmann',
'Hauptfeuerwehrfrau',
'Löschmeister', 'Löschmeister',
'Oberlöschmeister', 'Oberlöschmeister',
'Hauptlöschmeister', 'Hauptlöschmeister',
@@ -19,8 +17,12 @@ export const DIENSTGRAD_VALUES = [
'Hauptbrandmeister', 'Hauptbrandmeister',
'Brandinspektor', 'Brandinspektor',
'Oberbrandinspektor', 'Oberbrandinspektor',
'Brandoberinspektor', 'Verwaltungsmeister',
'Brandamtmann', 'Oberverwaltungsmeister',
'Hauptverwaltungsmeister',
'Verwalter',
'Sachbearbeiter',
'Abschnittssachbearbeiter',
] as const; ] as const;
export const STATUS_VALUES = [ export const STATUS_VALUES = [

View File

@@ -17,15 +17,11 @@ function log(msg: string) {
*/ */
function mapDienstgrad(raw: string): string | null { function mapDienstgrad(raw: string): string | null {
const abbrevMap: Record<string, string> = { const abbrevMap: Record<string, string> = {
'fa': 'Feuerwehranwärter',
'jfm': 'Jugendfeuerwehrmann', 'jfm': 'Jugendfeuerwehrmann',
'pfm': 'Probefeuerwehrmann', 'pfm': 'Probefeuerwehrmann',
'fm': 'Feuerwehrmann', 'fm': 'Feuerwehrmann',
'ff': 'Feuerwehrfrau',
'ofm': 'Oberfeuerwehrmann', 'ofm': 'Oberfeuerwehrmann',
'off': 'Oberfeuerwehrfrau',
'hfm': 'Hauptfeuerwehrmann', 'hfm': 'Hauptfeuerwehrmann',
'hff': 'Hauptfeuerwehrfrau',
'lm': 'Löschmeister', 'lm': 'Löschmeister',
'olm': 'Oberlöschmeister', 'olm': 'Oberlöschmeister',
'hlm': 'Hauptlöschmeister', 'hlm': 'Hauptlöschmeister',
@@ -34,13 +30,12 @@ function mapDienstgrad(raw: string): string | null {
'hbm': 'Hauptbrandmeister', 'hbm': 'Hauptbrandmeister',
'bi': 'Brandinspektor', 'bi': 'Brandinspektor',
'obi': 'Oberbrandinspektor', 'obi': 'Oberbrandinspektor',
'boi': 'Brandoberinspektor',
'bam': 'Brandamtmann',
'vm': 'Verwaltungsmeister', 'vm': 'Verwaltungsmeister',
'ovm': 'Oberverwaltungsmeister', 'ovm': 'Oberverwaltungsmeister',
'hvm': 'Hauptverwaltungsmeister', 'hvm': 'Hauptverwaltungsmeister',
'v': 'Verwalter', 'v': 'Verwalter',
'sb': 'Sachbearbeiter', 'sb': 'Sachbearbeiter',
'asb': 'Abschnittssachbearbeiter',
}; };
const normalized = raw.trim().toLowerCase().replace(/\*/g, ''); const normalized = raw.trim().toLowerCase().replace(/\*/g, '');