feat(ausruestungsanfrage): show vendor order status and delivery progress in request detail
This commit is contained in:
@@ -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<number, {
|
||||
bestellung_id: number;
|
||||
bestellung_bezeichnung: string;
|
||||
bestellung_status: string;
|
||||
bestellung_laufende_nummer: number | null;
|
||||
bestellung_erstellt_am: string;
|
||||
erhalten_menge: number;
|
||||
bestellt_menge: number;
|
||||
}> = {};
|
||||
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;
|
||||
|
||||
@@ -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 && (
|
||||
<Chip label="Im Haus" size="small" color="success" />
|
||||
)}
|
||||
{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 (
|
||||
<Chip
|
||||
label={`${kennung} · ${bi.erhalten_menge}/${bi.bestellt_menge} erhalten`}
|
||||
size="small"
|
||||
color={bi.erhalten_menge >= 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' && (
|
||||
<Chip label="Nicht zugewiesen" size="small" color="default" variant="outlined" />
|
||||
)}
|
||||
@@ -475,11 +502,46 @@ export default function AusruestungsanfrageDetail() {
|
||||
{/* Linked Bestellungen */}
|
||||
{detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && (
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Verknüpfte Bestellungen</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{detail.linked_bestellungen.map(b => (
|
||||
<Chip key={b.id} label={`#${b.id} ${b.bezeichnung}`} size="small" />
|
||||
))}
|
||||
<Typography variant="subtitle2" sx={{ mb: 1.5 }}>Verknüpfte Bestellungen</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{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 (
|
||||
<Paper
|
||||
key={b.id}
|
||||
variant="outlined"
|
||||
onClick={() => navigate(`/bestellungen/${b.id}`)}
|
||||
sx={{ p: 1.5, cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" fontWeight={500} sx={{ flexGrow: 1 }}>
|
||||
{kennung} — {b.bezeichnung}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={BESTELLUNG_STATUS_LABELS[b.status as keyof typeof BESTELLUNG_STATUS_LABELS] ?? b.status}
|
||||
color={(BESTELLUNG_STATUS_COLORS[b.status as keyof typeof BESTELLUNG_STATUS_COLORS] as 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning') ?? 'default'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
{totalOrdered > 0 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.min(pct, 100)}
|
||||
color={pct >= 100 ? 'success' : 'primary'}
|
||||
sx={{ flexGrow: 1, height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{totalReceived}/{totalOrdered} erhalten
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user