diff --git a/DEVLOG.md b/DEVLOG.md index 773c689..a31ac72 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3506,3 +3506,39 @@ Keine Commits in dieser Session. - frontend/src/styles/global.css | 7 +++++++ --- +## 2026-09-04 22:35 – 22:35 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** frontend + +### Commits +- a7b670f Bestückung eines Objekts vollständig pflegbar (Sollmenge/Status/Neuanlage) + +### Geänderte Dateien +- DEVLOG.md | 13 ++++ +- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 221 ++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-04 22:45 – 22:45 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 13 ++++ +- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 221 ++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-04 22:45 – 22:45 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 13 ++++ +- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 221 ++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- diff --git a/frontend/DEVLOG.md b/frontend/DEVLOG.md index d8ad04e..ecc5d82 100644 --- a/frontend/DEVLOG.md +++ b/frontend/DEVLOG.md @@ -93,3 +93,46 @@ Keine Commits in dieser Session. - frontend/src/styles/global.css | 19 ++++++ --- +## 2026-09-04 22:30 – 22:30 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 24 ++++++++++++++++++++ +- backend/app/api/v1/endpoints/benutzer.py | 6 ++++- +- backend/tests/test_benutzer_admin.py | 37 ++++++++++++++++++++++++++++++ +- frontend/src/App.tsx | 7 ++++-- +- frontend/src/auth/AuthContext.tsx | 15 ++++++++++++ +- frontend/src/components/AppShell.tsx | 6 ++--- +- frontend/src/pages/AdminPage.tsx | 94 ++++++++++++++++++++++++++++++++++++++++++++++++---------------------------- +- frontend/src/pages/admin/ObjektSection.tsx | 42 ++++++++++++++++++++++++++-------- + +--- +## 2026-09-04 22:33 – 22:34 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/AdminPage.tsx | 4 ++-- +- frontend/src/styles/global.css | 7 +++++++ + +--- +## 2026-09-04 22:48 – 22:50 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 13 ++++ +- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 221 ++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- diff --git a/frontend/src/pages/KontrollPage.tsx b/frontend/src/pages/KontrollPage.tsx index cd7bb0c..637d3a4 100644 --- a/frontend/src/pages/KontrollPage.tsx +++ b/frontend/src/pages/KontrollPage.tsx @@ -2,35 +2,8 @@ import { useNavigate, useParams } from "react-router-dom"; import { NachfuellDialog } from "./kontrolle/NachfuellDialog"; import { PositionCard } from "./kontrolle/PositionCard"; -import type { PositionZustand } from "./kontrolle/types"; import { useKontrolle } from "./kontrolle/useKontrolle"; - -const OHNE_FACH = "Ohne Fach"; - -// Gruppierung nach Fach (Vorlagen-Feature "feste Fächer-Liste je Objekttyp"): -// Reihenfolge folgt dem ersten Auftreten in der Positionsliste (die wiederum -// die Vorlagen-Reihenfolge widerspiegelt), "Ohne Fach" immer zuletzt - so -// bleibt die Erfassung nah am Regal/Fahrzeug-Layout statt alphabetisch zu -// springen. -function gruppiereNachFach(positionen: PositionZustand[]): [string, PositionZustand[]][] { - const gruppen = new Map(); - for (const zustand of positionen) { - const fach = zustand.fach ?? OHNE_FACH; - const liste = gruppen.get(fach); - if (liste) { - liste.push(zustand); - } else { - gruppen.set(fach, [zustand]); - } - } - const eintraege = [...gruppen.entries()]; - eintraege.sort(([a], [b]) => { - if (a === OHNE_FACH) return 1; - if (b === OHNE_FACH) return -1; - return 0; - }); - return eintraege; -} +import { gruppiereNachFach, OHNE_FACH } from "../utils/fachGruppierung"; export function KontrollPage() { const { objektId } = useParams<{ objektId: string }>(); @@ -55,7 +28,7 @@ export function KontrollPage() { nachfuellDialogSchliessen, } = useKontrolle(objektId); - const fachGruppen = gruppiereNachFach(positionen); + const fachGruppen = gruppiereNachFach(positionen, (zustand) => zustand.fach); const nurOhneFach = fachGruppen.length === 1 && fachGruppen[0][0] === OHNE_FACH; async function handleAbschliessen() { diff --git a/frontend/src/pages/admin/ObjektPositionenPanel.tsx b/frontend/src/pages/admin/ObjektPositionenPanel.tsx index 717e980..55f613c 100644 --- a/frontend/src/pages/admin/ObjektPositionenPanel.tsx +++ b/frontend/src/pages/admin/ObjektPositionenPanel.tsx @@ -1,11 +1,13 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { apiRequest } from "../../api/client"; -import type { Material, Objektposition } from "../../api/types"; +import type { Beladungsvorlage, Material, Objektposition } from "../../api/types"; +import { gruppiereNachFach, OHNE_FACH } from "../../utils/fachGruppierung"; import { GeraeteInstanzenListe } from "./GeraeteInstanzenListe"; interface Props { objektId: number; + vorlageId: number | null; materialien: Material[]; onFehler: (text: string) => void; } @@ -34,19 +36,30 @@ function leeresFormular(p: Objektposition): ZeilenFormular { * 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. + * + * Nutzer-Vorgabe: Fach-Sidebar links, damit die Bestückung übersichtlich bleibt + * UND damit auch Dritte (Vertretung, wenn die Kontrollperson nicht da ist) + * auf einen Blick sehen, wo welches Material im Rucksack hingehört - gleiche + * Fach-Zuordnung wie in der Kontroll-Erfassung (Vorlagenposition.fach). */ -export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props) { +export function ObjektPositionenPanel({ objektId, vorlageId, materialien, onFehler }: Props) { const [positionen, setPositionen] = useState([]); + const [vorlage, setVorlage] = useState(null); const [formulare, setFormulare] = useState>({}); const [speichertId, setSpeichertId] = useState(null); const [neuesMaterialId, setNeuesMaterialId] = useState(""); const [neueSollmenge, setNeueSollmenge] = useState(""); const [wirdHinzugefuegt, setWirdHinzugefuegt] = useState(false); + const [aktivesFach, setAktivesFach] = useState(null); async function laden() { try { - const liste = await apiRequest(`/objekte/${objektId}/positionen`); + const [liste, vorlageDaten] = await Promise.all([ + apiRequest(`/objekte/${objektId}/positionen`), + vorlageId ? apiRequest(`/vorlagen/${vorlageId}`) : Promise.resolve(null), + ]); setPositionen(liste); + setVorlage(vorlageDaten); setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)]))); } catch { onFehler("Positionen konnten nicht geladen werden."); @@ -55,8 +68,25 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props useEffect(() => { laden(); + setAktivesFach(null); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [objektId]); + }, [objektId, vorlageId]); + + const fachByMaterialId = useMemo(() => { + const map = new Map(); + for (const vp of vorlage?.positionen ?? []) { + map.set(vp.material_id, vp.fach); + } + return map; + }, [vorlage]); + + const fachGruppen = useMemo( + () => gruppiereNachFach(positionen, (p) => fachByMaterialId.get(p.material_id) ?? null), + [positionen, fachByMaterialId] + ); + const sichtbarePositionen = aktivesFach + ? fachGruppen.find(([fach]) => fach === aktivesFach)?.[1] ?? [] + : positionen; function materialName(materialId: number) { return materialien.find((m) => m.id === materialId)?.name ?? `Material ${materialId}`; @@ -156,8 +186,34 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props {positionen.length === 0 &&

Keine Positionen in diesem Objekt.

} + {positionen.length > 0 && ( +
+ + +
    - {positionen.map((p) => { + {sichtbarePositionen.map((p) => { const typ = materialtyp(p.material_id); const formular = formulare[p.id] ?? leeresFormular(p); const entfernt = p.ist_status === "entfernt"; @@ -166,6 +222,7 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
    {materialName(p.material_id)} + {fachByMaterialId.get(p.material_id) ?? OHNE_FACH} {typ} {entfernt && entfernt} @@ -238,6 +295,9 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props ); })}
+
+
+ )} ); } diff --git a/frontend/src/pages/admin/ObjektSection.tsx b/frontend/src/pages/admin/ObjektSection.tsx index 708f976..fd69f57 100644 --- a/frontend/src/pages/admin/ObjektSection.tsx +++ b/frontend/src/pages/admin/ObjektSection.tsx @@ -288,7 +288,12 @@ export function ObjektSection({ )} {ausgeklapptId === o.id && (
- +
)} {duplizierId === o.id && ( diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index b3f4266..4e3d575 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -110,9 +110,12 @@ p { /* Verwaltung (Admin/Material-/Leitungsverantwortliche) läuft am Desktop - Tabellen (Änderungslog) und mehrspaltige Formulare brauchen mehr Platz als - die mobile 640px-Kontroll-Ansicht der Mitarbeiter. */ + die mobile 640px-Kontroll-Ansicht der Mitarbeiter. Nutzer-Vorgabe: links + ausgerichtet statt zentriert - bei 1200px sonst viel verschenkter Platz + rechts/links auf breiten Monitoren. */ .page-wide { max-width: 1200px; + margin: 0; } /* -- Cards ---------------------------------------------------------------- */ diff --git a/frontend/src/utils/fachGruppierung.ts b/frontend/src/utils/fachGruppierung.ts new file mode 100644 index 0000000..e82b132 --- /dev/null +++ b/frontend/src/utils/fachGruppierung.ts @@ -0,0 +1,30 @@ +export const OHNE_FACH = "Ohne Fach"; + +/** + * Gruppierung nach Fach (Vorlagen-Feature "feste Fächer-Liste je Objekttyp"): + * Reihenfolge folgt dem ersten Auftreten in der Eingabeliste (die wiederum + * die Vorlagen-Reihenfolge widerspiegelt), "Ohne Fach" immer zuletzt - bleibt + * damit nah am Regal-/Fahrzeug-Layout statt alphabetisch zu springen. + * Geteilt zwischen Kontroll-Erfassung (KontrollPage) und Bestückungs-Pflege + * (ObjektPositionenPanel) - gleiche Gruppierungslogik, unterschiedliche + * Eintragstypen. + */ +export function gruppiereNachFach(eintraege: T[], fachVon: (eintrag: T) => string | null): [string, T[]][] { + const gruppen = new Map(); + for (const eintrag of eintraege) { + const fach = fachVon(eintrag) ?? OHNE_FACH; + const liste = gruppen.get(fach); + if (liste) { + liste.push(eintrag); + } else { + gruppen.set(fach, [eintrag]); + } + } + const gruppenListe = [...gruppen.entries()]; + gruppenListe.sort(([a], [b]) => { + if (a === OHNE_FACH) return 1; + if (b === OHNE_FACH) return -1; + return 0; + }); + return gruppenListe; +}