From eb2342684e8b83a332a07e194a93017e02825dd7 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Wed, 15 Apr 2026 18:17:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(bestellungen):=20add=20optional=20"F=C3=BC?= =?UTF-8?q?r=20Mitglied"=20field,=20auto-populated=20from=20internal=20req?= =?UTF-8?q?uest=20submitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/bestellung.controller.ts | 6 ++- .../089_add_mitglied_to_bestellungen.sql | 3 ++ .../services/ausruestungsanfrage.service.ts | 13 +++++-- backend/src/services/bestellung.service.ts | 25 +++++++++---- frontend/src/pages/BestellungDetail.tsx | 37 +++++++++++++++++++ frontend/src/pages/BestellungNeu.tsx | 10 ++++- frontend/src/pages/Bestellungen.tsx | 1 + frontend/src/types/bestellung.types.ts | 4 ++ 8 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 backend/src/database/migrations/089_add_mitglied_to_bestellungen.sql diff --git a/backend/src/controllers/bestellung.controller.ts b/backend/src/controllers/bestellung.controller.ts index 1558493..2a6bbdb 100644 --- a/backend/src/controllers/bestellung.controller.ts +++ b/backend/src/controllers/bestellung.controller.ts @@ -193,7 +193,7 @@ class BestellungController { } async createOrder(req: Request, res: Response): Promise { - const { bezeichnung, lieferant_id, budget, besteller_id, positionen } = req.body; + const { bezeichnung, lieferant_id, budget, besteller_id, positionen, mitglied_id } = req.body; if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) { res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' }); return; @@ -210,6 +210,10 @@ class BestellungController { res.status(400).json({ success: false, message: 'Ungültige Besteller-ID' }); return; } + if (mitglied_id != null && mitglied_id !== '' && (typeof mitglied_id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(mitglied_id))) { + res.status(400).json({ success: false, message: 'Ungültige Mitglied-ID' }); + return; + } if (positionen != null && !Array.isArray(positionen)) { res.status(400).json({ success: false, message: 'Positionen muss ein Array sein' }); return; diff --git a/backend/src/database/migrations/089_add_mitglied_to_bestellungen.sql b/backend/src/database/migrations/089_add_mitglied_to_bestellungen.sql new file mode 100644 index 0000000..0181652 --- /dev/null +++ b/backend/src/database/migrations/089_add_mitglied_to_bestellungen.sql @@ -0,0 +1,3 @@ +-- Add optional mitglied_id (the member an order is for) to bestellungen +ALTER TABLE bestellungen + ADD COLUMN IF NOT EXISTS mitglied_id UUID REFERENCES users(id) ON DELETE SET NULL; diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index e52336a..41feb71 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -809,6 +809,13 @@ async function createOrdersFromRequest( const createdBestellungen: Array<{ id: number; bezeichnung: string; lieferant_name: string }> = []; + // Fetch anfrager_id so we can set mitglied_id on created orders + const anfrageResult = await client.query( + 'SELECT anfrager_id FROM ausruestung_anfragen WHERE id = $1', + [anfrageId] + ); + const anfragerId: string | null = anfrageResult.rows[0]?.anfrager_id ?? null; + for (const orderData of orders) { const nrResult = await client.query( `SELECT COALESCE(MAX(laufende_nummer), 0) + 1 AS next_nr @@ -818,10 +825,10 @@ async function createOrdersFromRequest( const laufendeNummer = nrResult.rows[0].next_nr; const bestellungResult = await client.query( - `INSERT INTO bestellungen (bezeichnung, lieferant_id, status, laufende_nummer, erstellt_von) - VALUES ($1, $2, 'wartet_auf_genehmigung', $3, $4) + `INSERT INTO bestellungen (bezeichnung, lieferant_id, status, mitglied_id, laufende_nummer, erstellt_von) + VALUES ($1, $2, 'wartet_auf_genehmigung', $3, $4, $5) RETURNING id, bezeichnung`, - [orderData.bezeichnung, orderData.lieferant_id, laufendeNummer, userId] + [orderData.bezeichnung, orderData.lieferant_id, anfragerId, laufendeNummer, userId] ); const bestellung = bestellungResult.rows[0]; diff --git a/backend/src/services/bestellung.service.ts b/backend/src/services/bestellung.service.ts index 5264650..cdfb5f9 100644 --- a/backend/src/services/bestellung.service.ts +++ b/backend/src/services/bestellung.service.ts @@ -153,6 +153,7 @@ async function getOrders(filters?: { status?: string; lieferant_id?: number; bes `SELECT b.*, l.name AS lieferant_name, COALESCE(u.name, u.preferred_username, u.email) AS besteller_name, + COALESCE(mu.name, mu.preferred_username, mu.email) AS mitglied_name, COALESCE(pos.total_cost, 0) AS total_cost, COALESCE(pos.items_count, 0) AS items_count, COALESCE(pos.total_received, 0) AS total_received, @@ -160,6 +161,7 @@ async function getOrders(filters?: { status?: string; lieferant_id?: number; bes FROM bestellungen b LEFT JOIN lieferanten l ON l.id = b.lieferant_id LEFT JOIN users u ON u.id = b.erstellt_von + LEFT JOIN users mu ON mu.id = b.mitglied_id LEFT JOIN LATERAL ( SELECT SUM(einzelpreis * menge) AS total_cost, COUNT(*) AS items_count, @@ -191,11 +193,16 @@ async function getOrderById(id: number) { COALESCE(u.name, u.preferred_username, u.email) AS besteller_name, u.email AS besteller_email, COALESCE(mp.telefon_mobil, mp.telefon_privat) AS besteller_telefon, - mp.dienstgrad AS besteller_dienstgrad + mp.dienstgrad AS besteller_dienstgrad, + mu.id AS mitglied_id, + COALESCE(mu.name, mu.preferred_username, mu.email) AS mitglied_name, + mmp.dienstgrad AS mitglied_dienstgrad FROM bestellungen b LEFT JOIN lieferanten l ON l.id = b.lieferant_id LEFT JOIN users u ON u.id = COALESCE(b.besteller_id, b.erstellt_von) LEFT JOIN mitglieder_profile mp ON mp.user_id = COALESCE(b.besteller_id, b.erstellt_von) + LEFT JOIN users mu ON mu.id = b.mitglied_id + LEFT JOIN mitglieder_profile mmp ON mmp.user_id = b.mitglied_id WHERE b.id = $1`, [id] ); @@ -227,7 +234,7 @@ async function getOrderById(id: number) { } } -async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number; steuersatz?: number; positionen?: Array<{ bezeichnung: string; menge: number; einheit?: string }> }, userId: string) { +async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; mitglied_id?: string; notizen?: string; budget?: number; steuersatz?: number; positionen?: Array<{ bezeichnung: string; menge: number; einheit?: string }> }, userId: string) { const client = await pool.connect(); try { await client.query('BEGIN'); @@ -242,10 +249,10 @@ async function createOrder(data: { bezeichnung: string; lieferant_id?: number; b const bestellerId = data.besteller_id && data.besteller_id.trim() ? data.besteller_id.trim() : null; const result = await client.query( - `INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, steuersatz, laufende_nummer, erstellt_von) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + `INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, steuersatz, mitglied_id, laufende_nummer, erstellt_von) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, - [data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, data.steuersatz ?? 20, laufendeNummer, userId] + [data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, data.steuersatz ?? 20, data.mitglied_id || null, laufendeNummer, userId] ); const order = result.rows[0]; @@ -271,7 +278,7 @@ async function createOrder(data: { bezeichnung: string; lieferant_id?: number; b } } -async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_id?: number; besteller_id?: string | null; notizen?: string; budget?: number; status?: string; steuersatz?: number }, userId: string) { +async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_id?: number; besteller_id?: string | null; notizen?: string; budget?: number; status?: string; steuersatz?: number; mitglied_id?: string | null }, userId: string) { try { // Check current order for status change detection const current = await pool.query(`SELECT * FROM bestellungen WHERE id = $1`, [id]); @@ -303,10 +310,11 @@ async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_i bestellt_am = $7, abgeschlossen_am = $8, steuersatz = COALESCE($9, steuersatz), + mitglied_id = CASE WHEN $10::text IS NOT NULL AND $10::text != '' THEN $10::uuid ELSE mitglied_id END, aktualisiert_am = NOW() - WHERE id = $10 + WHERE id = $11 RETURNING *`, - [data.bezeichnung, data.lieferant_id, data.besteller_id ?? null, data.notizen, data.budget, data.status, bestellt_am, abgeschlossen_am, data.steuersatz, id] + [data.bezeichnung, data.lieferant_id, data.besteller_id ?? null, data.notizen, data.budget, data.status, bestellt_am, abgeschlossen_am, data.steuersatz, data.mitglied_id ?? null, id] ); if (result.rows.length === 0) return null; @@ -317,6 +325,7 @@ async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_i if (data.status && data.status !== oldStatus) changes.push(`Status: ${oldStatus} → ${data.status}`); if (data.budget) changes.push(`Budget geändert`); if (data.steuersatz != null) changes.push(`Steuersatz: ${data.steuersatz}%`); + if (data.mitglied_id) changes.push('Mitglied geändert'); await logAction(id, 'Bestellung aktualisiert', changes.join(', ') || 'Bestellung bearbeitet', userId); return result.rows[0]; diff --git a/frontend/src/pages/BestellungDetail.tsx b/frontend/src/pages/BestellungDetail.tsx index 8a40444..4183b59 100644 --- a/frontend/src/pages/BestellungDetail.tsx +++ b/frontend/src/pages/BestellungDetail.tsx @@ -480,6 +480,7 @@ export default function BestellungDetail() { bezeichnung: string; lieferant_id?: number; besteller_id?: string; + mitglied_id?: string; notizen: string; steuersatz: number; }>({ bezeichnung: '', notizen: '', steuersatz: 20 }); @@ -661,6 +662,7 @@ export default function BestellungDetail() { bezeichnung: bestellung.bezeichnung, lieferant_id: bestellung.lieferant_id, besteller_id: bestellung.besteller_id || '', + mitglied_id: bestellung.mitglied_id || '', notizen: bestellung.notizen || '', steuersatz: parseFloat(String(bestellung.steuersatz ?? 20)), }); @@ -690,6 +692,7 @@ export default function BestellungDetail() { bezeichnung: editOrderData.bezeichnung, lieferant_id: editOrderData.lieferant_id, besteller_id: editOrderData.besteller_id || undefined, + mitglied_id: editOrderData.mitglied_id || undefined, notizen: editOrderData.notizen, steuersatz: editOrderData.steuersatz, }); @@ -822,6 +825,19 @@ export default function BestellungDetail() { curY += 3; } + // ── Für Mitglied block ── + if (bestellung.mitglied_name) { + doc.setFontSize(10); + doc.setFont('helvetica', 'bold'); + doc.text('Für Mitglied', 10, curY); + curY += 5; + const mitgliedNameWithRank = bestellung.mitglied_dienstgrad + ? `${kurzDienstgrad(bestellung.mitglied_dienstgrad)} ${bestellung.mitglied_name}` + : bestellung.mitglied_name; + row('Name', mitgliedNameWithRank); + curY += 3; + } + // ── Order info block ── doc.setFontSize(10); doc.setFont('helvetica', 'bold'); @@ -1115,6 +1131,15 @@ export default function BestellungDetail() { renderInput={(params) => } /> + + o.name} + value={orderUsers.find(u => u.id === editOrderData.mitglied_id) ?? null} + onChange={(_, v) => setEditOrderData(d => ({ ...d, mitglied_id: v?.id || '' }))} + renderInput={(params) => } + /> + {bestellung.besteller_name || '–'} + {bestellung.mitglied_name && ( + + + Für Mitglied + + {bestellung.mitglied_dienstgrad + ? `${kurzDienstgrad(bestellung.mitglied_dienstgrad)} ${bestellung.mitglied_name}` + : bestellung.mitglied_name} + + + + )} Erstellt am diff --git a/frontend/src/pages/BestellungNeu.tsx b/frontend/src/pages/BestellungNeu.tsx index 5893115..829b698 100644 --- a/frontend/src/pages/BestellungNeu.tsx +++ b/frontend/src/pages/BestellungNeu.tsx @@ -23,7 +23,7 @@ import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage'; import type { BestellungFormData, BestellpositionFormData, LieferantFormData, Lieferant } from '../types/bestellung.types'; import type { AusruestungArtikel } from '../types/ausruestungsanfrage.types'; -const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', notizen: '', positionen: [] }; +const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', mitglied_id: undefined, notizen: '', positionen: [] }; const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' }; const emptyPosition: BestellpositionFormData = { bezeichnung: '', menge: 1, einheit: 'Stk' }; @@ -150,6 +150,14 @@ export default function BestellungNeu() { renderInput={(params) => } /> + o.name} + value={orderUsers.find((u) => u.id === orderForm.mitglied_id) || null} + onChange={(_e, v) => setOrderForm((f) => ({ ...f, mitglied_id: v?.id || '' }))} + renderInput={(params) => } + /> + o.lieferant_name || '–' }, { key: 'besteller_name', label: 'Besteller', render: (o) => o.besteller_name || '–' }, + { key: 'mitglied_name', label: 'Für Mitglied', render: (o) => o.mitglied_name || '–' }, { key: 'status', label: 'Status', render: (o) => ( )}, diff --git a/frontend/src/types/bestellung.types.ts b/frontend/src/types/bestellung.types.ts index 7ef149c..703d64b 100644 --- a/frontend/src/types/bestellung.types.ts +++ b/frontend/src/types/bestellung.types.ts @@ -64,6 +64,9 @@ export interface Bestellung { besteller_email?: string; besteller_telefon?: string; besteller_dienstgrad?: string; + mitglied_id?: string; + mitglied_name?: string; + mitglied_dienstgrad?: string; status: BestellungStatus; budget?: number; steuersatz?: number; @@ -88,6 +91,7 @@ export interface BestellungFormData { bezeichnung: string; lieferant_id?: number; besteller_id?: string; + mitglied_id?: string; status?: BestellungStatus; steuersatz?: number; notizen?: string;