fix(sync): add Sachbearbeiter to dienstgrad constraint; add catalog browser dialog for external order position

This commit is contained in:
Matthias Hochmeister
2026-04-15 18:05:39 +02:00
parent 9586822a32
commit 67fd0878ce
5 changed files with 486 additions and 32 deletions

View File

@@ -61,7 +61,7 @@ class MemberController {
status: normalizeArray(statusParam) as any,
dienstgrad: normalizeArray(dienstgradParam) as any,
page: page ? parseInt(page, 10) || 1 : 1,
pageSize: pageSize ? Math.min(parseInt(pageSize, 10) || 25, 100) : 25,
pageSize: pageSize ? (parseInt(pageSize, 10) === 0 ? 0 : Math.min(parseInt(pageSize, 10) || 25, 100)) : 25,
});
res.status(200).json({

View File

@@ -0,0 +1,57 @@
-- Migration 088: Add 'Sachbearbeiter' to mitglieder_profile dienstgrad CHECK constraint.
-- 'SB' is a valid FDISK Dienstgrad abbreviation that was missing from the allowed list,
-- causing the FDISK sync to fail when a member's current rank is Sachbearbeiter.
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
'Feuerwehranwärter',
'Jugendfeuerwehrmann',
'Probefeuerwehrmann',
'Feuerwehrmann',
'Feuerwehrfrau',
'Oberfeuerwehrmann',
'Oberfeuerwehrfrau',
'Hauptfeuerwehrmann',
'Hauptfeuerwehrfrau',
'Löschmeister',
'Oberlöschmeister',
'Hauptlöschmeister',
'Brandmeister',
'Oberbrandmeister',
'Hauptbrandmeister',
'Brandinspektor',
'Oberbrandinspektor',
'Brandoberinspektor',
'Brandamtmann',
'Verwaltungsmeister',
'Oberverwaltungsmeister',
'Hauptverwaltungsmeister',
'Verwalter',
'Sachbearbeiter',
-- Ehrendienstgrade
'Ehren-Feuerwehrmann',
'Ehren-Feuerwehrfrau',
'Ehren-Oberfeuerwehrmann',
'Ehren-Oberfeuerwehrfrau',
'Ehren-Hauptfeuerwehrmann',
'Ehren-Hauptfeuerwehrfrau',
'Ehren-Löschmeister',
'Ehren-Oberlöschmeister',
'Ehren-Hauptlöschmeister',
'Ehren-Brandmeister',
'Ehren-Oberbrandmeister',
'Ehren-Hauptbrandmeister',
'Ehren-Brandinspektor',
'Ehren-Oberbrandinspektor',
'Ehren-Brandoberinspektor',
'Ehren-Brandamtmann',
'Ehren-Verwaltungsmeister',
'Ehren-Oberverwaltungsmeister',
'Ehren-Hauptverwaltungsmeister',
'Ehren-Verwalter'
));

View File

@@ -163,31 +163,57 @@ class MemberService {
}
const whereClause = `WHERE ${conditions.join(' AND ')}`;
const offset = (page - 1) * pageSize;
const fetchAll = pageSize === 0;
const dataQuery = `
SELECT
u.id,
u.name,
u.given_name,
u.family_name,
u.email,
u.profile_picture_url,
u.is_active,
mp.id AS profile_id,
mp.fdisk_standesbuch_nr,
mp.dienstgrad,
mp.funktion,
mp.status,
mp.eintrittsdatum,
mp.telefon_mobil
FROM users u
LEFT JOIN mitglieder_profile mp ON mp.user_id = u.id
${whereClause}
ORDER BY u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
`;
values.push(pageSize, offset);
let dataQuery: string;
if (fetchAll) {
dataQuery = `
SELECT
u.id,
u.name,
u.given_name,
u.family_name,
u.email,
u.profile_picture_url,
u.is_active,
mp.id AS profile_id,
mp.fdisk_standesbuch_nr,
mp.dienstgrad,
mp.funktion,
mp.status,
mp.eintrittsdatum,
mp.telefon_mobil
FROM users u
LEFT JOIN mitglieder_profile mp ON mp.user_id = u.id
${whereClause}
ORDER BY u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST
`;
} else {
const offset = (page - 1) * pageSize;
dataQuery = `
SELECT
u.id,
u.name,
u.given_name,
u.family_name,
u.email,
u.profile_picture_url,
u.is_active,
mp.id AS profile_id,
mp.fdisk_standesbuch_nr,
mp.dienstgrad,
mp.funktion,
mp.status,
mp.eintrittsdatum,
mp.telefon_mobil
FROM users u
LEFT JOIN mitglieder_profile mp ON mp.user_id = u.id
${whereClause}
ORDER BY u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
`;
values.push(pageSize, offset);
}
const countQuery = `
SELECT COUNT(*)::INTEGER AS total
@@ -196,9 +222,11 @@ class MemberService {
${whereClause}
`;
const countValues = fetchAll ? values : values.slice(0, values.length - 2);
const [dataResult, countResult] = await Promise.all([
pool.query(dataQuery, values),
pool.query(countQuery, values.slice(0, values.length - 2)), // exclude LIMIT/OFFSET
pool.query(countQuery, countValues),
]);
const items: MemberListItem[] = dataResult.rows.map((row) => ({