ObjektPositionenPanel: Fach-Sidebar (Vorlagen-Feature "feste Fächer-Liste") links, damit die Bestückung übersichtlich bleibt und auch Dritte (Vertretung ohne die Kontrollperson) sehen, wo welches Material im Rucksack hingehört. Gruppierungslogik aus KontrollPage in gemeinsames Util (utils/fachGruppierung) ausgelagert, da jetzt zweimal gebraucht. .page-wide: margin:0 statt zentriert (Nutzer-Vorgabe: bei 1200px sonst viel verschenkter Platz links/rechts auf breiten Monitoren). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import { useNavigate, useParams } from "react-router-dom";
|
||
|
||
import { NachfuellDialog } from "./kontrolle/NachfuellDialog";
|
||
import { PositionCard } from "./kontrolle/PositionCard";
|
||
import { useKontrolle } from "./kontrolle/useKontrolle";
|
||
import { gruppiereNachFach, OHNE_FACH } from "../utils/fachGruppierung";
|
||
|
||
export function KontrollPage() {
|
||
const { objektId } = useParams<{ objektId: string }>();
|
||
const navigate = useNavigate();
|
||
const {
|
||
online,
|
||
objekt,
|
||
sperre,
|
||
positionen,
|
||
startFehler,
|
||
wirdAbgeschlossen,
|
||
abschlussFehler,
|
||
alleUebertragen,
|
||
nachfuellDialog,
|
||
starteKontrolle,
|
||
eingabeAendern,
|
||
positionSenden,
|
||
fehlerErneutVersuchen,
|
||
abschliessen,
|
||
abbrechen,
|
||
jetztNachfuellen,
|
||
nachfuellDialogSchliessen,
|
||
} = useKontrolle(objektId);
|
||
|
||
const fachGruppen = gruppiereNachFach(positionen, (zustand) => zustand.fach);
|
||
const nurOhneFach = fachGruppen.length === 1 && fachGruppen[0][0] === OHNE_FACH;
|
||
|
||
async function handleAbschliessen() {
|
||
if (await abschliessen()) {
|
||
navigate("/objekte");
|
||
}
|
||
}
|
||
|
||
async function handleAbbrechen() {
|
||
const grund = window.prompt("Grund für den Abbruch (optional):") ?? undefined;
|
||
await abbrechen(grund);
|
||
navigate("/objekte");
|
||
}
|
||
|
||
if (sperre) {
|
||
return (
|
||
<main className="page">
|
||
<h1>Objekt gesperrt</h1>
|
||
<div className="card">
|
||
<p>
|
||
Wird gerade kontrolliert von <strong>{sperre.benutzer ?? "unbekannt"}</strong>, seit {sperre.seit}.
|
||
</p>
|
||
<button className="btn btn-primary" onClick={() => starteKontrolle(true)}>
|
||
Trotzdem übernehmen
|
||
</button>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
if (startFehler) {
|
||
return (
|
||
<main className="page">
|
||
<div className="alert alert-danger">{startFehler}</div>
|
||
<button className="btn btn-secondary" onClick={() => starteKontrolle(false)}>
|
||
Erneut versuchen
|
||
</button>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<main className="page">
|
||
<h1>{objekt?.name ?? "Kontrolle"}</h1>
|
||
{positionen.length > 0 && (
|
||
<div className="card" style={{ marginBottom: "1rem" }}>
|
||
<div className="card-subtitle">
|
||
{positionen.filter((p) => p.status === "gespeichert").length} von {positionen.length} Positionen bestätigt
|
||
</div>
|
||
<div style={{ height: "6px", background: "var(--color-border, #e2e8f0)", borderRadius: "3px", marginTop: "0.35rem" }}>
|
||
<div
|
||
style={{
|
||
height: "100%",
|
||
borderRadius: "3px",
|
||
background: "var(--color-primary, #2563eb)",
|
||
width: `${Math.round(
|
||
(positionen.filter((p) => p.status === "gespeichert").length / positionen.length) * 100
|
||
)}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{!online && (
|
||
<div className="alert alert-warning">
|
||
Kein Netz – Eingaben werden lokal gespeichert und automatisch übertragen, sobald wieder Verbindung besteht.
|
||
</div>
|
||
)}
|
||
{fachGruppen.map(([fach, gruppenPositionen]) => (
|
||
<div key={fach} style={{ marginBottom: "1.25rem" }}>
|
||
{!nurOhneFach && <h2 style={{ fontSize: "1rem", marginBottom: "0.5rem" }}>{fach}</h2>}
|
||
<ul className="card-list">
|
||
{gruppenPositionen.map((zustand) => (
|
||
<li key={zustand.position.id} style={{ listStyle: "none" }}>
|
||
<PositionCard
|
||
zustand={zustand}
|
||
onEingabeAendern={eingabeAendern}
|
||
onSenden={positionSenden}
|
||
onErneutVersuchen={fehlerErneutVersuchen}
|
||
/>
|
||
{nachfuellDialog?.materialId === zustand.position.material_id && (
|
||
<NachfuellDialog
|
||
materialName={zustand.material?.name ?? "Material"}
|
||
vorschlagMenge={String(
|
||
Number(zustand.position.sollmenge_effektiv) - Number(zustand.eingabe)
|
||
)}
|
||
onNachfuellen={(menge) => jetztNachfuellen(nachfuellDialog.fehlbestandId, menge)}
|
||
onSchliessen={nachfuellDialogSchliessen}
|
||
/>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
|
||
{abschlussFehler && <div className="alert alert-danger">{abschlussFehler}</div>}
|
||
<div className="stack">
|
||
<button
|
||
className="btn btn-primary btn-block"
|
||
onClick={handleAbschliessen}
|
||
disabled={!alleUebertragen || wirdAbgeschlossen}
|
||
>
|
||
{alleUebertragen ? "Kontrolle abschließen" : "Noch nicht alle Positionen gespeichert…"}
|
||
</button>
|
||
<button className="btn btn-danger btn-block" onClick={handleAbbrechen}>
|
||
Kontrolle abbrechen
|
||
</button>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|