From 2b77ae5724862be91b96e0eab8d024fc0b51a5e5 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 24 Mar 2026 10:22:31 +0100 Subject: [PATCH] rework internal order system --- .../050_add_missing_anfragen_columns.sql | 22 +++++ .../services/ausruestungsanfrage.service.ts | 82 +++++++++++----- backend/src/services/cleanup.service.ts | 10 +- frontend/src/pages/Ausruestungsanfrage.tsx | 95 +++++++++++++------ 4 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 backend/src/database/migrations/050_add_missing_anfragen_columns.sql diff --git a/backend/src/database/migrations/050_add_missing_anfragen_columns.sql b/backend/src/database/migrations/050_add_missing_anfragen_columns.sql new file mode 100644 index 0000000..6b4d1c9 --- /dev/null +++ b/backend/src/database/migrations/050_add_missing_anfragen_columns.sql @@ -0,0 +1,22 @@ +-- Migration 050: Add missing columns to ausruestung_anfragen +-- These columns are referenced by the service code but were never created + +ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bearbeitet_am TIMESTAMPTZ; +ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bestell_nummer INT; +ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bestell_jahr INT; + +-- Backfill bestell_nummer for existing rows +DO $$ +DECLARE + yr INT; + rec RECORD; + nr INT; +BEGIN + FOR yr IN SELECT DISTINCT EXTRACT(YEAR FROM erstellt_am)::int FROM ausruestung_anfragen LOOP + nr := 0; + FOR rec IN SELECT id FROM ausruestung_anfragen WHERE EXTRACT(YEAR FROM erstellt_am)::int = yr AND bestell_nummer IS NULL ORDER BY erstellt_am LOOP + nr := nr + 1; + UPDATE ausruestung_anfragen SET bestell_nummer = nr, bestell_jahr = yr WHERE id = rec.id; + END LOOP; + END LOOP; +END $$; diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index 02345b0..54529fd 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -393,21 +393,36 @@ async function createRequest( await client.query('BEGIN'); const currentYear = new Date().getFullYear(); - const maxResult = await client.query( - `SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr - FROM ausruestung_anfragen - WHERE bestell_jahr = $1`, - [currentYear], - ); - const nextNr = maxResult.rows[0].next_nr; - const anfrageResult = await client.query( - `INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr) - VALUES ($1, $2, $3, $4, $5) - RETURNING *`, - [userId, notizen || null, bezeichnung || null, nextNr, currentYear], - ); - const anfrage = anfrageResult.rows[0]; + // Try with bestell_nummer/bestell_jahr (migration 050), fallback without + let anfrage: Record; + try { + await client.query('SAVEPOINT sp_bestell_nr'); + const maxResult = await client.query( + `SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr + FROM ausruestung_anfragen + WHERE bestell_jahr = $1`, + [currentYear], + ); + const nextNr = maxResult.rows[0].next_nr; + const anfrageResult = await client.query( + `INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [userId, notizen || null, bezeichnung || null, nextNr, currentYear], + ); + await client.query('RELEASE SAVEPOINT sp_bestell_nr'); + anfrage = anfrageResult.rows[0]; + } catch { + await client.query('ROLLBACK TO SAVEPOINT sp_bestell_nr'); + const anfrageResult = await client.query( + `INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung) + VALUES ($1, $2, $3) + RETURNING *`, + [userId, notizen || null, bezeichnung || null], + ); + anfrage = anfrageResult.rows[0]; + } for (const item of items) { let itemBezeichnung = item.bezeichnung; @@ -566,17 +581,34 @@ async function updateRequestStatus( adminNotizen?: string, bearbeitetVon?: string, ) { - const result = await pool.query( - `UPDATE ausruestung_anfragen - SET status = $1, - admin_notizen = COALESCE($2, admin_notizen), - bearbeitet_von = COALESCE($3, bearbeitet_von), - bearbeitet_am = NOW() - WHERE id = $4 - RETURNING *`, - [status, adminNotizen || null, bearbeitetVon || null, id], - ); - return result.rows[0] || null; + // Use aktualisiert_am (always exists) + try bearbeitet_am (added in migration 050) + try { + const result = await pool.query( + `UPDATE ausruestung_anfragen + SET status = $1, + admin_notizen = COALESCE($2, admin_notizen), + bearbeitet_von = COALESCE($3, bearbeitet_von), + bearbeitet_am = NOW(), + aktualisiert_am = NOW() + WHERE id = $4 + RETURNING *`, + [status, adminNotizen || null, bearbeitetVon || null, id], + ); + return result.rows[0] || null; + } catch { + // Fallback if bearbeitet_am column doesn't exist yet (migration 050 not run) + const result = await pool.query( + `UPDATE ausruestung_anfragen + SET status = $1, + admin_notizen = COALESCE($2, admin_notizen), + bearbeitet_von = COALESCE($3, bearbeitet_von), + aktualisiert_am = NOW() + WHERE id = $4 + RETURNING *`, + [status, adminNotizen || null, bearbeitetVon || null, id], + ); + return result.rows[0] || null; + } } async function deleteRequest(id: number) { diff --git a/backend/src/services/cleanup.service.ts b/backend/src/services/cleanup.service.ts index 6a5177e..bf849ba 100644 --- a/backend/src/services/cleanup.service.ts +++ b/backend/src/services/cleanup.service.ts @@ -134,8 +134,10 @@ class CleanupService { } const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen'); const count = rows[0].count; + // Delete related linking tables first, then main table + await pool.query('DELETE FROM ausruestung_anfrage_bestellung WHERE bestellung_id IN (SELECT id FROM bestellungen)'); await pool.query('TRUNCATE bestellungen CASCADE'); - await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1'); + try { await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ } logger.info(`Cleanup: truncated bestellungen (${count} rows) and reset sequence`); return { count, deleted: true }; } @@ -147,8 +149,10 @@ class CleanupService { } const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen'); const count = rows[0].count; + // Delete linking table first + await pool.query('DELETE FROM ausruestung_anfrage_bestellung WHERE anfrage_id IN (SELECT id FROM ausruestung_anfragen)'); await pool.query('TRUNCATE ausruestung_anfragen CASCADE'); - await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1'); + try { await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ } logger.info(`Cleanup: truncated ausruestung_anfragen (${count} rows) and reset sequence`); return { count, deleted: true }; } @@ -161,7 +165,7 @@ class CleanupService { const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues'); const count = rows[0].count; await pool.query('TRUNCATE issues CASCADE'); - await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1'); + try { await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ } logger.info(`Cleanup: truncated issues (${count} rows) and reset sequence`); return { count, deleted: true }; } diff --git a/frontend/src/pages/Ausruestungsanfrage.tsx b/frontend/src/pages/Ausruestungsanfrage.tsx index 32e617a..9aaa880 100644 --- a/frontend/src/pages/Ausruestungsanfrage.tsx +++ b/frontend/src/pages/Ausruestungsanfrage.tsx @@ -518,39 +518,74 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can ) : ( /* ── View Mode ── */ <> - {anfrage!.anfrager_name && ( - - Anfrager: {anfrage!.anfrager_name} - - )} + {/* Meta info */} + + {anfrage!.anfrager_name && ( + + Anfrager + {anfrage!.anfrager_name} + + )} + + Erstellt am + {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')} + + {anfrage!.bearbeitet_von_name && ( + + Bearbeitet von + {anfrage!.bearbeitet_von_name} + + )} + + {anfrage!.notizen && ( - Notizen: {anfrage!.notizen} + + Notizen + {anfrage!.notizen} + )} {anfrage!.admin_notizen && ( - Admin Notizen: {anfrage!.admin_notizen} + + Admin Notizen + {anfrage!.admin_notizen} + )} - - Erstellt am: {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')} - - Positionen - {detail.positionen.map(p => ( - - - - {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''} - - {p.eigenschaften && p.eigenschaften.length > 0 && ( - - {p.eigenschaften.map(e => ( - - {e.eigenschaft_name}: {e.wert} - - ))} - - )} - - ))} + + {/* Positionen */} + Positionen ({detail.positionen.length}) + + + + Artikel + Menge + Details + + + + {detail.positionen.map(p => ( + + + {p.bezeichnung} + {p.eigenschaften && p.eigenschaften.length > 0 && ( + + {p.eigenschaften.map(e => ( + + ))} + + )} + + + {p.menge}x + + + {p.notizen && {p.notizen}} + + + ))} + +
{detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( <> @@ -1252,14 +1287,14 @@ function MeineAnfragenTab() { function AlleAnfragenTab() { const { hasPermission } = usePermissionContext(); const { user } = useAuth(); - const [statusFilter, setStatusFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState('alle'); const [detailId, setDetailId] = useState(null); const canEditAny = hasPermission('ausruestungsanfrage:edit'); const { data: requests = [], isLoading: requestsLoading, isError: requestsError } = useQuery({ queryKey: ['ausruestungsanfrage', 'allRequests', statusFilter], - queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter ? { status: statusFilter } : undefined), + queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter !== 'alle' ? { status: statusFilter } : undefined), }); const { data: overview } = useQuery({ @@ -1305,7 +1340,7 @@ function AlleAnfragenTab() { onChange={e => setStatusFilter(e.target.value)} sx={{ minWidth: 200, mb: 2 }} > - Alle + Alle {(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => ( {AUSRUESTUNG_STATUS_LABELS[s]} ))}