feat(material): Material-Dubletten zusammenführen
Verwendung je Material einsehbar (Objektposition/Vorlagenposition/
Bestand/Kontrollposition/Fehlbestand/Ausgabe/Nachfüllung/
Materialbewegung), POST /materialien/{id}/ersetzen hängt alle
Referenzen auf ein Ziel-Material um und löscht die Quelle. Bricht bei
Konflikten ab (z.B. dasselbe Objekt hätte danach zwei Positionen für
dasselbe Material) statt Daten stillschweigend zu verlieren. Admin-UI:
"Zusammenführen" je Material zeigt Verwendung + Ziel-Auswahl.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -28,6 +28,48 @@ export function MaterialSection({ materialien, onGeaendert, onFehler }: Props) {
|
||||
const [wirdGespeichert, setWirdGespeichert] = useState(false);
|
||||
const [suche, setSuche] = useState("");
|
||||
|
||||
const [mergeQuelleId, setMergeQuelleId] = useState<number | null>(null);
|
||||
const [mergeVerwendung, setMergeVerwendung] = useState<Record<string, number> | null>(null);
|
||||
const [mergeZielId, setMergeZielId] = useState<number | "">("");
|
||||
const [mergeKonflikte, setMergeKonflikte] = useState<string[]>([]);
|
||||
|
||||
async function zusammenfuehrenOeffnen(material: Material) {
|
||||
if (mergeQuelleId === material.id) {
|
||||
setMergeQuelleId(null);
|
||||
return;
|
||||
}
|
||||
setMergeQuelleId(material.id);
|
||||
setMergeZielId("");
|
||||
setMergeKonflikte([]);
|
||||
setMergeVerwendung(null);
|
||||
try {
|
||||
setMergeVerwendung(await apiRequest<Record<string, number>>(`/materialien/${material.id}/verwendung`));
|
||||
} catch {
|
||||
onFehler("Verwendung konnte nicht geladen werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function zusammenfuehren() {
|
||||
if (mergeQuelleId === null || mergeZielId === "") {
|
||||
onFehler("Ziel-Material auswählen.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ergebnis = await apiRequest<{ erfolgreich: boolean; konflikte: string[] }>(
|
||||
`/materialien/${mergeQuelleId}/ersetzen`,
|
||||
{ method: "POST", body: { ziel_material_id: mergeZielId } }
|
||||
);
|
||||
if (!ergebnis.erfolgreich) {
|
||||
setMergeKonflikte(ergebnis.konflikte);
|
||||
return;
|
||||
}
|
||||
setMergeQuelleId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Zusammenführen fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(material: Material) {
|
||||
setBearbeiteteId(material.id);
|
||||
setFormular({
|
||||
@@ -149,19 +191,70 @@ export function MaterialSection({ materialien, onGeaendert, onFehler }: Props) {
|
||||
{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 key={m.id} className="card" style={{ padding: "0.6rem 1rem" }}>
|
||||
<div className="row-between">
|
||||
<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>
|
||||
<span className="row" style={{ gap: "0.4rem" }}>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(m)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => zusammenfuehrenOeffnen(m)}>
|
||||
Zusammenführen
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{mergeQuelleId === m.id && (
|
||||
<div className="card" style={{ marginTop: "0.5rem", background: "var(--color-surface-sunken)" }}>
|
||||
{mergeVerwendung === null ? (
|
||||
<p className="text-muted">Lade Verwendung…</p>
|
||||
) : (
|
||||
<ul>
|
||||
{Object.entries(mergeVerwendung).map(([tabelle, anzahl]) => (
|
||||
<li key={tabelle}>
|
||||
{tabelle}: {anzahl}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{mergeKonflikte.length > 0 && (
|
||||
<div className="alert alert-danger">
|
||||
Konflikt(e), Zusammenführen abgebrochen:
|
||||
<ul>
|
||||
{mergeKonflikte.map((k) => (
|
||||
<li key={k}>{k}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="row" style={{ marginTop: "0.5rem" }}>
|
||||
<select
|
||||
className="input"
|
||||
value={mergeZielId}
|
||||
onChange={(e) => setMergeZielId(Number(e.target.value) || "")}
|
||||
>
|
||||
<option value="">Ziel-Material wählen…</option>
|
||||
{materialien
|
||||
.filter((z) => z.id !== m.id)
|
||||
.map((z) => (
|
||||
<option key={z.id} value={z.id}>
|
||||
{z.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn btn-danger" onClick={zusammenfuehren}>
|
||||
{m.name} löschen, alles auf Ziel umhängen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user