Files
MABEA/frontend/src/pages/admin/ObjektPositionenPanel.tsx
T
patrickandClaude Sonnet 5 a6e68b0a94
CI / backend-tests (push) Successful in 53s
CI / frontend-build (push) Successful in 15s
Sprint 8: Ablaufdatum/Chargennummer/Seriennummer im Admin-Portal pflegbar
Nutzer-Fund: bei keinem Objekt war Chargennummer/Ablaufdatum sichtbar.
Backend-Endpoint (PATCH /objekte/{id}/positionen/{position_id}) existierte
bereits (Prompt 10), war im Frontend nirgends angebunden. Kontroll-Screen
selbst bleibt unverändert (Vier-Kernbegriffe: Kontrolle erfasst nur Menge,
Ablauf/Charge/SN gehören zur Objektposition) - neue Pflege-Ansicht im
Admin-Portal pro Objekt, nur für materialtyp ablauf_charge/geraet_sn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
2026-09-04 01:03:37 +02:00

134 lines
4.6 KiB
TypeScript

import { useEffect, useState } from "react";
import { apiRequest } from "../../api/client";
import type { Material, Objektposition } from "../../api/types";
interface Props {
objektId: number;
materialien: Material[];
onFehler: (text: string) => void;
}
interface ZeilenFormular {
ablaufdatum: string;
chargennummer: string;
seriennummer: string;
}
function leeresFormular(p: Objektposition): ZeilenFormular {
return {
ablaufdatum: p.ablaufdatum ?? "",
chargennummer: p.chargennummer ?? "",
seriennummer: p.seriennummer ?? "",
};
}
/**
* Prompt 10: Ablaufdatum/Chargennummer (materialtyp 'ablauf_charge') und
* Seriennummer (materialtyp 'geraet_sn') gehören zur Objektposition, nicht
* zur Kontrolle selbst (Vier-Kernbegriffe: Kontrolle erfasst nur die Menge).
* Backend-Endpoint (PATCH .../positionen/{id}) existierte schon, war im
* Frontend bisher nirgends angebunden.
*/
export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props) {
const [positionen, setPositionen] = useState<Objektposition[]>([]);
const [formulare, setFormulare] = useState<Record<string, ZeilenFormular>>({});
const [speichertId, setSpeichertId] = useState<string | null>(null);
useEffect(() => {
apiRequest<Objektposition[]>(`/objekte/${objektId}/positionen`)
.then((liste) => {
setPositionen(liste);
setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)])));
})
.catch(() => onFehler("Positionen konnten nicht geladen werden."));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [objektId]);
function materialName(materialId: number) {
return materialien.find((m) => m.id === materialId)?.name ?? `Material ${materialId}`;
}
function materialtyp(materialId: number) {
return materialien.find((m) => m.id === materialId)?.materialtyp;
}
async function speichern(position: Objektposition) {
const formular = formulare[position.id];
setSpeichertId(position.id);
try {
const aktualisiert = await apiRequest<Objektposition>(`/objekte/${objektId}/positionen/${position.id}`, {
method: "PATCH",
body: {
ablaufdatum: formular.ablaufdatum || null,
chargennummer: formular.chargennummer || null,
seriennummer: formular.seriennummer || null,
},
});
setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p)));
} catch {
onFehler("Position konnte nicht gespeichert werden.");
} finally {
setSpeichertId(null);
}
}
const relevante = positionen.filter((p) => materialtyp(p.material_id) !== "standard");
if (relevante.length === 0) {
return <p className="text-muted">Kein Material mit Ablaufdatum/Charge oder Seriennummer in diesem Objekt.</p>;
}
return (
<ul className="card-list">
{relevante.map((p) => {
const typ = materialtyp(p.material_id);
const formular = formulare[p.id] ?? leeresFormular(p);
return (
<li key={p.id} className="card">
<div className="card-title-row">
<strong>{materialName(p.material_id)}</strong>
<span className="badge badge-neutral">{typ}</span>
</div>
<div className="row" style={{ marginTop: "0.6rem" }}>
{typ === "ablauf_charge" && (
<>
<input
type="date"
className="input"
value={formular.ablaufdatum}
onChange={(e) =>
setFormulare((f) => ({ ...f, [p.id]: { ...formular, ablaufdatum: e.target.value } }))
}
/>
<input
className="input"
placeholder="Chargennummer"
value={formular.chargennummer}
onChange={(e) =>
setFormulare((f) => ({ ...f, [p.id]: { ...formular, chargennummer: e.target.value } }))
}
/>
</>
)}
{typ === "geraet_sn" && (
<input
className="input"
placeholder="Seriennummer"
value={formular.seriennummer}
onChange={(e) =>
setFormulare((f) => ({ ...f, [p.id]: { ...formular, seriennummer: e.target.value } }))
}
/>
)}
<button className="btn btn-primary" onClick={() => speichern(p)} disabled={speichertId === p.id}>
Speichern
</button>
</div>
</li>
);
})}
</ul>
);
}