Bestückung eines Objekts vollständig pflegbar (Sollmenge/Status/Neuanlage)
ObjektPositionenPanel zeigte bisher nur ablauf_charge/geraet_sn-Positionen und konnte weder Sollmenge-Override noch Status noch neues Material ändern - Backend-Endpunkte (PATCH/POST .../positionen) existierten schon, waren im Frontend nicht angebunden. Jetzt: alle Positionen sichtbar, Sollmenge-Override je Position, Entfernen/ Reaktivieren (ist_status), zusätzliches Material hinzufügen (Dropdown mit noch nicht vorhandenen Materialien + Sollmenge). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -3493,3 +3493,16 @@ Keine Commits in dieser Session.
|
||||
- frontend/src/pages/admin/ObjektSection.tsx | 42 ++++++++++++++++++++++++++--------
|
||||
|
||||
---
|
||||
## 2026-09-04 22:31 – 22:31 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
- a2b201d Verwaltung: breiteres Layout für Admin/Material-/Leitungsverantwortliche
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 4 ++--
|
||||
- frontend/src/styles/global.css | 7 +++++++
|
||||
|
||||
---
|
||||
|
||||
@@ -11,6 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
interface ZeilenFormular {
|
||||
sollmengeOverride: string;
|
||||
ablaufdatum: string;
|
||||
chargennummer: string;
|
||||
pruefintervallMonate: string;
|
||||
@@ -18,6 +19,7 @@ interface ZeilenFormular {
|
||||
|
||||
function leeresFormular(p: Objektposition): ZeilenFormular {
|
||||
return {
|
||||
sollmengeOverride: p.sollmenge_override ?? "",
|
||||
ablaufdatum: p.ablaufdatum ?? "",
|
||||
chargennummer: p.chargennummer ?? "",
|
||||
pruefintervallMonate: p.pruefintervall_monate?.toString() ?? "",
|
||||
@@ -25,24 +27,34 @@ function leeresFormular(p: Objektposition): ZeilenFormular {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Bestückung eines Objekts pflegen (Prompt 10): Sollmenge-Override je Position
|
||||
* (Abweichung vom Vorlagen-Soll), Position entfernen/reaktivieren, zusätzliches
|
||||
* Material hinzufügen das nicht in der Vorlage steht - plus Ablaufdatum/Charge/
|
||||
* Prüfintervall/Geräte-Instanzen für die entsprechenden Materialtypen. Backend-
|
||||
* Endpunkte existierten schon (PATCH/POST .../positionen), waren im Frontend
|
||||
* bisher nur für ablauf_charge/geraet_sn und nicht für Sollmenge/Status/Neuanlage
|
||||
* 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);
|
||||
const [neuesMaterialId, setNeuesMaterialId] = useState<number | "">("");
|
||||
const [neueSollmenge, setNeueSollmenge] = useState("");
|
||||
const [wirdHinzugefuegt, setWirdHinzugefuegt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiRequest<Objektposition[]>(`/objekte/${objektId}/positionen`)
|
||||
.then((liste) => {
|
||||
async function laden() {
|
||||
try {
|
||||
const liste = await apiRequest<Objektposition[]>(`/objekte/${objektId}/positionen`);
|
||||
setPositionen(liste);
|
||||
setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)])));
|
||||
})
|
||||
.catch(() => onFehler("Positionen konnten nicht geladen werden."));
|
||||
} catch {
|
||||
onFehler("Positionen konnten nicht geladen werden.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [objektId]);
|
||||
|
||||
@@ -61,6 +73,7 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
||||
const aktualisiert = await apiRequest<Objektposition>(`/objekte/${objektId}/positionen/${position.id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
sollmenge_override: formular.sollmengeOverride === "" ? null : Number(formular.sollmengeOverride),
|
||||
ablaufdatum: formular.ablaufdatum || null,
|
||||
chargennummer: formular.chargennummer || null,
|
||||
pruefintervall_monate: formular.pruefintervallMonate ? Number(formular.pruefintervallMonate) : null,
|
||||
@@ -74,24 +87,104 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
async function statusUmschalten(position: Objektposition) {
|
||||
const neuerStatus = position.ist_status === "aktiv" ? "entfernt" : "aktiv";
|
||||
try {
|
||||
const aktualisiert = await apiRequest<Objektposition>(`/objekte/${objektId}/positionen/${position.id}`, {
|
||||
method: "PATCH",
|
||||
body: { ist_status: neuerStatus },
|
||||
});
|
||||
setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p)));
|
||||
} catch {
|
||||
onFehler("Status konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function materialHinzufuegen() {
|
||||
if (neuesMaterialId === "" || !neueSollmenge) return;
|
||||
setWirdHinzugefuegt(true);
|
||||
try {
|
||||
await apiRequest(`/objekte/${objektId}/positionen`, {
|
||||
method: "POST",
|
||||
body: { material_id: neuesMaterialId, sollmenge_override: Number(neueSollmenge) },
|
||||
});
|
||||
setNeuesMaterialId("");
|
||||
setNeueSollmenge("");
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Material konnte nicht hinzugefügt werden (evtl. schon vorhanden).");
|
||||
} finally {
|
||||
setWirdHinzugefuegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
const vorhandeneMaterialIds = new Set(positionen.map((p) => p.material_id));
|
||||
const verfuegbareMaterialien = materialien.filter((m) => !vorhandeneMaterialIds.has(m.id));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row" style={{ flexWrap: "wrap", marginBottom: "1rem" }}>
|
||||
<select
|
||||
className="input"
|
||||
value={neuesMaterialId}
|
||||
onChange={(e) => setNeuesMaterialId(Number(e.target.value) || "")}
|
||||
>
|
||||
<option value="">Material hinzufügen…</option>
|
||||
{verfuegbareMaterialien.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className="input"
|
||||
style={{ maxWidth: "8rem" }}
|
||||
placeholder="Sollmenge"
|
||||
value={neueSollmenge}
|
||||
onChange={(e) => setNeueSollmenge(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={materialHinzufuegen}
|
||||
disabled={neuesMaterialId === "" || !neueSollmenge || wirdHinzugefuegt}
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{positionen.length === 0 && <p className="text-muted">Keine Positionen in diesem Objekt.</p>}
|
||||
|
||||
<ul className="card-list">
|
||||
{relevante.map((p) => {
|
||||
{positionen.map((p) => {
|
||||
const typ = materialtyp(p.material_id);
|
||||
const formular = formulare[p.id] ?? leeresFormular(p);
|
||||
const entfernt = p.ist_status === "entfernt";
|
||||
return (
|
||||
<li key={p.id} className="card">
|
||||
<li key={p.id} className="card" style={entfernt ? { opacity: 0.6 } : undefined}>
|
||||
<div className="card-title-row">
|
||||
<strong>{materialName(p.material_id)}</strong>
|
||||
<span className="row" style={{ gap: "0.4rem" }}>
|
||||
<span className="badge badge-neutral">{typ}</span>
|
||||
{entfernt && <span className="badge badge-danger">entfernt</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-subtitle">Soll (Vorlage/aktuell wirksam): {p.sollmenge_effektiv}</div>
|
||||
<div className="row" style={{ marginTop: "0.6rem", flexWrap: "wrap" }}>
|
||||
<div className="field" style={{ maxWidth: "10rem" }}>
|
||||
<label>Sollmenge-Override</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className="input"
|
||||
placeholder="wie Vorlage"
|
||||
value={formular.sollmengeOverride}
|
||||
onChange={(e) =>
|
||||
setFormulare((f) => ({ ...f, [p.id]: { ...formular, sollmengeOverride: e.target.value } }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="row" style={{ marginTop: "0.6rem" }}>
|
||||
{typ === "ablauf_charge" && (
|
||||
<>
|
||||
<input
|
||||
@@ -128,7 +221,10 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
||||
/>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={() => speichern(p)} disabled={speichertId === p.id}>
|
||||
{typ === "geraet_sn" ? "Prüfintervall speichern" : "Speichern"}
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => statusUmschalten(p)}>
|
||||
{entfernt ? "Reaktivieren" : "Entfernen"}
|
||||
</button>
|
||||
</div>
|
||||
{typ === "geraet_sn" && (
|
||||
@@ -142,5 +238,6 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user