diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index 00c4652..bddee96 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -409,19 +409,64 @@ async function getRequestById(id: number) { } catch { /* table may not exist */ } } + // Load per-position vendor order info (anfrage_position_id FK, added in migration 096) + let positionBestellInfoMap: Record = {}; + if (positionIds.length > 0) { + try { + const bpResult = await pool.query( + `SELECT bp.anfrage_position_id, + bp.bestellung_id, + bp.erhalten_menge, + bp.menge AS bestellt_menge, + b.bezeichnung AS bestellung_bezeichnung, + b.status AS bestellung_status, + b.laufende_nummer AS bestellung_laufende_nummer, + b.erstellt_am AS bestellung_erstellt_am + FROM bestellpositionen bp + JOIN bestellungen b ON b.id = bp.bestellung_id + WHERE bp.anfrage_position_id = ANY($1)`, + [positionIds], + ); + for (const row of bpResult.rows) { + positionBestellInfoMap[row.anfrage_position_id] = { + bestellung_id: row.bestellung_id, + bestellung_bezeichnung: row.bestellung_bezeichnung, + bestellung_status: row.bestellung_status, + bestellung_laufende_nummer: row.bestellung_laufende_nummer ?? null, + bestellung_erstellt_am: row.bestellung_erstellt_am, + erhalten_menge: Number(row.erhalten_menge), + bestellt_menge: Number(row.bestellt_menge), + }; + } + } catch { /* bestellpositionen.anfrage_position_id may not exist */ } + } + const positionenWithEigenschaften = positionen.rows.map((p: { id: number }) => ({ ...p, eigenschaften: eigenschaftenMap[p.id] || [], + bestellung_info: positionBestellInfoMap[p.id] ?? null, })); - // Load linked bestellungen + // Load linked bestellungen with delivery totals let linkedBestellungen: unknown[] = []; try { const bestellungen = await pool.query( - `SELECT b.* + `SELECT b.id, b.bezeichnung, b.status, b.laufende_nummer, b.erstellt_am, + COALESCE(SUM(bp.erhalten_menge), 0)::int AS total_received, + COALESCE(SUM(bp.menge), 0)::int AS total_ordered FROM ausruestung_anfrage_bestellung ab JOIN bestellungen b ON b.id = ab.bestellung_id - WHERE ab.anfrage_id = $1`, + LEFT JOIN bestellpositionen bp ON bp.bestellung_id = b.id + WHERE ab.anfrage_id = $1 + GROUP BY b.id, b.bezeichnung, b.status, b.laufende_nummer, b.erstellt_am`, [id], ); linkedBestellungen = bestellungen.rows; diff --git a/frontend/src/pages/AusruestungsanfrageDetail.tsx b/frontend/src/pages/AusruestungsanfrageDetail.tsx index b976aa5..3780fa1 100644 --- a/frontend/src/pages/AusruestungsanfrageDetail.tsx +++ b/frontend/src/pages/AusruestungsanfrageDetail.tsx @@ -19,6 +19,7 @@ import { usePermissionContext } from '../contexts/PermissionContext'; import { useAuth } from '../contexts/AuthContext'; import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage'; import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types'; +import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types'; import type { AusruestungAnfrage, AusruestungAnfrageDetailResponse, AusruestungAnfrageFormItem, AusruestungAnfrageStatus, @@ -34,6 +35,14 @@ function formatOrderId(r: AusruestungAnfrage): string { return `#${r.id}`; } +function formatBestellungKennung(b: { id: number; laufende_nummer?: number | null; erstellt_am?: string }): string { + if (b.laufende_nummer != null && b.erstellt_am) { + const year = new Date(b.erstellt_am).getFullYear(); + return `${year}/${b.laufende_nummer}`; + } + return `#${b.id}`; +} + // ── Helpers ── function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] { @@ -433,6 +442,24 @@ export default function AusruestungsanfrageDetail() { {p.im_haus && ( )} + {p.bestellung_info && (() => { + const bi = p.bestellung_info!; + const kennung = formatBestellungKennung({ + id: bi.bestellung_id, + laufende_nummer: bi.bestellung_laufende_nummer, + erstellt_am: bi.bestellung_erstellt_am, + }); + return ( + = bi.bestellt_menge ? 'success' : 'default'} + variant="outlined" + onClick={(e) => { e.stopPropagation(); navigate(`/bestellungen/${bi.bestellung_id}`); }} + sx={{ cursor: 'pointer' }} + /> + ); + })()} {p.geliefert && p.zuweisung_typ === 'keine' && ( )} @@ -475,11 +502,46 @@ export default function AusruestungsanfrageDetail() { {/* Linked Bestellungen */} {detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( - Verknüpfte Bestellungen - - {detail.linked_bestellungen.map(b => ( - - ))} + Verknüpfte Bestellungen + + {detail.linked_bestellungen.map(b => { + const totalOrdered = b.total_ordered ?? 0; + const totalReceived = b.total_received ?? 0; + const pct = totalOrdered > 0 ? (totalReceived / totalOrdered) * 100 : 0; + const kennung = formatBestellungKennung(b); + return ( + navigate(`/bestellungen/${b.id}`)} + sx={{ p: 1.5, cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} + > + + + {kennung} — {b.bezeichnung} + + + + {totalOrdered > 0 && ( + + = 100 ? 'success' : 'primary'} + sx={{ flexGrow: 1, height: 6, borderRadius: 3 }} + /> + + {totalReceived}/{totalOrdered} erhalten + + + )} + + ); + })} )} diff --git a/frontend/src/types/ausruestungsanfrage.types.ts b/frontend/src/types/ausruestungsanfrage.types.ts index 3ab08fe..9aef0df 100644 --- a/frontend/src/types/ausruestungsanfrage.types.ts +++ b/frontend/src/types/ausruestungsanfrage.types.ts @@ -120,6 +120,15 @@ export interface AusruestungAnfragePosition { persoenlich_id?: string | null; aktueller_zustand?: string | null; neuer_zustand?: string | null; + bestellung_info?: { + bestellung_id: number; + bestellung_bezeichnung: string; + bestellung_status: string; + bestellung_laufende_nummer?: number | null; + bestellung_erstellt_am?: string; + erhalten_menge: number; + bestellt_menge: number; + } | null; } export interface AusruestungAnfrageFormItem { @@ -138,7 +147,15 @@ export interface AusruestungAnfrageFormItem { export interface AusruestungAnfrageDetailResponse { anfrage: AusruestungAnfrage; positionen: AusruestungAnfragePosition[]; - linked_bestellungen?: { id: number; bezeichnung: string; status: string }[]; + linked_bestellungen?: { + id: number; + bezeichnung: string; + status: string; + laufende_nummer?: number; + erstellt_am?: string; + total_received?: number; + total_ordered?: number; + }[]; im_haus?: boolean; }