Ablauf/Charge/SN gehören zur Kontroll-Erfassung, nicht nur zur Admin-Pflege
CI / backend-tests (push) Successful in 54s
CI / frontend-build (push) Successful in 18s

Nutzer-Vorgabe: "Ablauf/Charge/SN gehört mit zu den Kontrollen wie auch die
Menge". PUT .../positionen/{material_id} akzeptiert jetzt optional
ablaufdatum/chargennummer/seriennummer und schreibt sie auf die
Objektposition (Vier-Kernbegriffe bleiben unberührt: nur diese drei Felder,
niemals istmenge - das bleibt exklusiv Nachfüllung vorbehalten). Offline-
Queue und PositionCard erweitert: Felder erscheinen nur passend zum
Materialtyp (ablauf_charge -> Datum+Charge, geraet_sn -> SN). Admin-Panel
(ObjektPositionenPanel) bleibt zusätzlich für nachträgliche Korrekturen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-04 01:11:17 +02:00
co-authored by Claude Sonnet 5
parent a6e68b0a94
commit 0988f8d026
8 changed files with 138 additions and 18 deletions
+39 -6
View File
@@ -3,12 +3,19 @@ import type { PositionZustand } from "./types";
interface Props {
zustand: PositionZustand;
onEingabeAendern: (materialId: number, wert: string) => void;
onSenden: (materialId: number, istmenge: string) => void;
onErneutVersuchen: (materialId: number, istmenge: string) => void;
onEingabeAendern: (materialId: number, feld: keyof PositionZustand, wert: string) => void;
onSenden: (materialId: number, zustand: PositionZustand) => void;
onErneutVersuchen: (materialId: number, zustand: PositionZustand) => void;
}
/**
* Nutzer-Vorgabe: Ablauf/Charge/SN gehören zur Kontroll-Erfassung dazu, wie
* die Menge - kein separater Admin-Schritt. Felder erscheinen nur, wenn der
* Materialtyp sie verlangt (ablauf_charge -> Ablauf+Charge, geraet_sn -> SN).
*/
export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVersuchen }: Props) {
const materialtyp = zustand.material?.materialtyp;
return (
<li className="card">
<div className="card-title-row">
@@ -23,17 +30,43 @@ export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVers
type="number"
className="input"
value={zustand.eingabe}
onChange={(e) => onEingabeAendern(zustand.position.material_id, e.target.value)}
onChange={(e) => onEingabeAendern(zustand.position.material_id, "eingabe", e.target.value)}
style={{ width: "6rem" }}
/>
<button
className="btn btn-primary"
style={{ flex: 1 }}
onClick={() => onSenden(zustand.position.material_id, zustand.eingabe)}
onClick={() => onSenden(zustand.position.material_id, zustand)}
>
Bestätigen
</button>
</div>
{materialtyp === "ablauf_charge" && (
<div className="row" style={{ marginTop: "0.5rem" }}>
<input
type="date"
className="input"
value={zustand.ablaufdatum}
onChange={(e) => onEingabeAendern(zustand.position.material_id, "ablaufdatum", e.target.value)}
/>
<input
className="input"
placeholder="Chargennummer"
value={zustand.chargennummer}
onChange={(e) => onEingabeAendern(zustand.position.material_id, "chargennummer", e.target.value)}
/>
</div>
)}
{materialtyp === "geraet_sn" && (
<div className="row" style={{ marginTop: "0.5rem" }}>
<input
className="input"
placeholder="Seriennummer"
value={zustand.seriennummer}
onChange={(e) => onEingabeAendern(zustand.position.material_id, "seriennummer", e.target.value)}
/>
</div>
)}
{zustand.status === "fehler" && (
<div style={{ marginTop: "0.6rem" }}>
<p className="text-muted" style={{ color: "var(--color-danger)" }}>
@@ -41,7 +74,7 @@ export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVers
</p>
<button
className="btn btn-secondary"
onClick={() => onErneutVersuchen(zustand.position.material_id, zustand.eingabe)}
onClick={() => onErneutVersuchen(zustand.position.material_id, zustand)}
>
Erneut versuchen
</button>
+3
View File
@@ -8,6 +8,9 @@ export interface PositionZustand {
material: Material | undefined;
status: PositionStatus;
eingabe: string;
ablaufdatum: string;
chargennummer: string;
seriennummer: string;
fehlerText: string | null;
}
+13 -6
View File
@@ -68,6 +68,9 @@ export function useKontrolle(objektId: string | undefined) {
material: materialById.get(p.material_id),
status: "unbestaetigt",
eingabe: p.sollmenge_effektiv,
ablaufdatum: p.ablaufdatum ?? "",
chargennummer: p.chargennummer ?? "",
seriennummer: p.seriennummer ?? "",
fehlerText: null,
}))
);
@@ -102,25 +105,29 @@ export function useKontrolle(objektId: string | undefined) {
if (online) void synchronisiereQueue();
}, [online]);
function eingabeAendern(materialId: number, wert: string) {
function eingabeAendern(materialId: number, feld: keyof PositionZustand, wert: string) {
setPositionen((bisherige) =>
bisherige.map((z) => (z.position.material_id === materialId ? { ...z, eingabe: wert } : z))
bisherige.map((z) => (z.position.material_id === materialId ? { ...z, [feld]: wert } : z))
);
}
async function positionSenden(materialId: number, istmenge: string) {
async function positionSenden(materialId: number, zustand: PositionZustand) {
if (!kontrolle) return;
await enqueue(kontrolle.id, materialId, istmenge);
await enqueue(kontrolle.id, materialId, zustand.eingabe, {
ablaufdatum: zustand.ablaufdatum || null,
chargennummer: zustand.chargennummer || null,
seriennummer: zustand.seriennummer || null,
});
setPositionen((bisherige) =>
bisherige.map((z) => (z.position.material_id === materialId ? { ...z, status: "wartet" } : z))
);
void synchronisiereQueue();
}
async function fehlerErneutVersuchen(materialId: number, istmenge: string) {
async function fehlerErneutVersuchen(materialId: number, zustand: PositionZustand) {
if (!kontrolle) return;
await entferneFehlerEintrag(kontrolle.id, materialId);
await positionSenden(materialId, istmenge);
await positionSenden(materialId, zustand);
}
const alleUebertragen = useMemo(