Formular (Anlegen/Bearbeiten) links fix, Liste rechts mit Suchfeld oben (Name/Artikelnummer) - bei 153 Materialien war die reine Scroll-Liste ohne Suche kaum nutzbar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import { useState } from "react";
|
|
|
|
import { apiRequest } from "../../api/client";
|
|
import type { Material } from "../../api/types";
|
|
|
|
const MATERIALTYPEN = [
|
|
{ value: "standard", label: "Standard" },
|
|
{ value: "ablauf_charge", label: "Ablauf/Charge" },
|
|
{ value: "geraet_sn", label: "Gerät mit Seriennummer" },
|
|
];
|
|
|
|
const LEERES_FORMULAR = {
|
|
name: "",
|
|
artikelnummer: "",
|
|
einheit: "Stück",
|
|
materialtyp: "standard",
|
|
};
|
|
|
|
interface Props {
|
|
materialien: Material[];
|
|
onGeaendert: () => Promise<void>;
|
|
onFehler: (text: string) => void;
|
|
}
|
|
|
|
export function MaterialSection({ materialien, onGeaendert, onFehler }: Props) {
|
|
const [formular, setFormular] = useState(LEERES_FORMULAR);
|
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
|
const [wirdGespeichert, setWirdGespeichert] = useState(false);
|
|
const [suche, setSuche] = useState("");
|
|
|
|
function bearbeiten(material: Material) {
|
|
setBearbeiteteId(material.id);
|
|
setFormular({
|
|
name: material.name,
|
|
artikelnummer: material.artikelnummer ?? "",
|
|
einheit: material.einheit,
|
|
materialtyp: material.materialtyp,
|
|
});
|
|
}
|
|
|
|
function zuruecksetzen() {
|
|
setBearbeiteteId(null);
|
|
setFormular(LEERES_FORMULAR);
|
|
}
|
|
|
|
async function speichern() {
|
|
setWirdGespeichert(true);
|
|
const body = {
|
|
name: formular.name,
|
|
artikelnummer: formular.artikelnummer || null,
|
|
einheit: formular.einheit,
|
|
materialtyp: formular.materialtyp,
|
|
};
|
|
try {
|
|
if (bearbeiteteId !== null) {
|
|
await apiRequest(`/materialien/${bearbeiteteId}`, { method: "PATCH", body });
|
|
} else {
|
|
await apiRequest("/materialien", { method: "POST", body });
|
|
}
|
|
zuruecksetzen();
|
|
await onGeaendert();
|
|
} catch {
|
|
onFehler("Material konnte nicht gespeichert werden.");
|
|
} finally {
|
|
setWirdGespeichert(false);
|
|
}
|
|
}
|
|
|
|
const sucheNormalisiert = suche.trim().toLowerCase();
|
|
const gefiltert = sucheNormalisiert
|
|
? materialien.filter(
|
|
(m) =>
|
|
m.name.toLowerCase().includes(sucheNormalisiert) ||
|
|
(m.artikelnummer ?? "").toLowerCase().includes(sucheNormalisiert)
|
|
)
|
|
: materialien;
|
|
|
|
return (
|
|
<section className="section">
|
|
<h2>Material</h2>
|
|
<div style={{ display: "flex", flexDirection: "row", alignItems: "flex-start", gap: "1.25rem" }}>
|
|
<div className="card" style={{ flex: "0 0 20rem", width: "20rem", boxSizing: "border-box" }}>
|
|
<h3 style={{ marginTop: 0 }}>{bearbeiteteId !== null ? "Material bearbeiten" : "Material anlegen"}</h3>
|
|
<div className="stack">
|
|
<div className="field">
|
|
<label>Name</label>
|
|
<input
|
|
className="input"
|
|
value={formular.name}
|
|
onChange={(e) => setFormular({ ...formular, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Artikelnummer</label>
|
|
<input
|
|
className="input"
|
|
value={formular.artikelnummer}
|
|
onChange={(e) => setFormular({ ...formular, artikelnummer: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Einheit</label>
|
|
<input
|
|
className="input"
|
|
value={formular.einheit}
|
|
onChange={(e) => setFormular({ ...formular, einheit: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Materialtyp</label>
|
|
<select
|
|
className="input"
|
|
value={formular.materialtyp}
|
|
onChange={(e) => setFormular({ ...formular, materialtyp: e.target.value })}
|
|
>
|
|
{MATERIALTYPEN.map((t) => (
|
|
<option key={t.value} value={t.value}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="row">
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={speichern}
|
|
disabled={!formular.name || !formular.einheit || wirdGespeichert}
|
|
>
|
|
{bearbeiteteId !== null ? "Speichern" : "Anlegen"}
|
|
</button>
|
|
{bearbeiteteId !== null && (
|
|
<button className="btn btn-secondary" onClick={zuruecksetzen}>
|
|
Abbrechen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ flex: "1 1 0%", minWidth: 0 }}>
|
|
<input
|
|
className="input input-search"
|
|
style={{ marginBottom: "1rem" }}
|
|
placeholder={`Suche in ${materialien.length} Materialien…`}
|
|
value={suche}
|
|
onChange={(e) => setSuche(e.target.value)}
|
|
/>
|
|
{gefiltert.length === 0 && <p className="text-muted">Keine Treffer.</p>}
|
|
{gefiltert.length > 0 && (
|
|
<ul className="card-list">
|
|
{gefiltert.map((m) => (
|
|
<li key={m.id} className="card row-between" style={{ padding: "0.6rem 1rem" }}>
|
|
<span>
|
|
{m.name} <span className="text-muted">({m.einheit})</span>
|
|
{m.artikelnummer && <span className="text-muted"> · {m.artikelnummer}</span>}
|
|
{m.materialtyp !== "standard" && (
|
|
<span className="badge badge-neutral" style={{ marginLeft: "0.5rem" }}>
|
|
{m.materialtyp}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<button className="btn btn-secondary" onClick={() => bearbeiten(m)}>
|
|
Bearbeiten
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|