From fbae3fccc726dc44cf10a38a230e406abbb55775 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 20:13:38 +0200 Subject: [PATCH] Kontroll-Erfassung: Positionen nach Fach gruppieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useKontrolle lädt zusätzlich die Vorlage und mappt Fach je Material. KontrollPage gruppiert die Erfassung nach Fach (Vorlagen-Reihenfolge, "Ohne Fach" zuletzt, keine Überschrift falls alles ohne Fach) statt flacher Liste - näher am Regal-/Fahrzeug-Layout. E2E-Mocks um Vorlage-Route ergänzt. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 30 ++++++++ backend/DEVLOG.md | 15 ++++ frontend/e2e/mockApi.ts | 10 +++ frontend/src/pages/KontrollPage.tsx | 78 ++++++++++++++------ frontend/src/pages/kontrolle/types.ts | 4 + frontend/src/pages/kontrolle/useKontrolle.ts | 13 +++- 6 files changed, 125 insertions(+), 25 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 4fd8b97..13d8feb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3028,3 +3028,33 @@ Keine Commits in dieser Session. - frontend/src/styles/global.css | 19 ++++++ --- +## 2026-09-04 20:03 – 20:04 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** frontend + +### Commits +- 5805366 Admin-Portal: Eskalations-Konfiguration-UI (Karte 12) + +### Geänderte Dateien +- DEVLOG.md | 20 ++++ +- frontend/DEVLOG.md | 40 ++++++++ +- frontend/src/api/types.ts | 5 + +- frontend/src/pages/AdminPage.tsx | 13 ++- +- frontend/src/pages/admin/EskalationSection.tsx | 127 +++++++++++++++++++++++++ + +--- +## 2026-09-04 20:09 – 20:10 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 20 ++++ +- frontend/DEVLOG.md | 40 ++++++++ +- frontend/src/api/types.ts | 5 + +- frontend/src/pages/AdminPage.tsx | 13 ++- +- frontend/src/pages/admin/EskalationSection.tsx | 127 +++++++++++++++++++++++++ + +--- diff --git a/backend/DEVLOG.md b/backend/DEVLOG.md index ca50937..8910ac3 100644 --- a/backend/DEVLOG.md +++ b/backend/DEVLOG.md @@ -116,3 +116,18 @@ Keine Commits in dieser Session. - ...07_indizes_und_unique_constraints.py => 0007_indizes_und_locks.py} | 4 ++-- --- +## 2026-09-04 20:10 – 20:13 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 20 ++++ +- frontend/DEVLOG.md | 40 ++++++++ +- frontend/src/api/types.ts | 5 + +- frontend/src/pages/AdminPage.tsx | 13 ++- +- frontend/src/pages/admin/EskalationSection.tsx | 127 +++++++++++++++++++++++++ + +--- diff --git a/frontend/e2e/mockApi.ts b/frontend/e2e/mockApi.ts index 33193c3..532665f 100644 --- a/frontend/e2e/mockApi.ts +++ b/frontend/e2e/mockApi.ts @@ -43,6 +43,15 @@ export const TEST_POSITION = { chargennummer: null, }; +export const TEST_VORLAGE = { + id: 1, + objekttyp_id: 1, + name: "Test-Vorlage", + version: 1, + status: "aktiv", + positionen: [{ id: 1, vorlage_id: 1, material_id: TEST_MATERIAL.id, fach: null, sollmenge: "10" }], +}; + export const TEST_KONTROLLE = { id: "kontrolle-1", objekt_id: 1, @@ -88,6 +97,7 @@ export async function mockKontrollGrundgeruest(page: Page): Promise { route.fulfill({ json: [TEST_POSITION] }) ); await page.route("**/api/v1/materialien", (route) => route.fulfill({ json: [TEST_MATERIAL] })); + await page.route(`**/api/v1/vorlagen/${TEST_VORLAGE.id}`, (route) => route.fulfill({ json: TEST_VORLAGE })); } export async function login(page: Page): Promise { diff --git a/frontend/src/pages/KontrollPage.tsx b/frontend/src/pages/KontrollPage.tsx index 392f009..cd7bb0c 100644 --- a/frontend/src/pages/KontrollPage.tsx +++ b/frontend/src/pages/KontrollPage.tsx @@ -2,8 +2,36 @@ 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; +} + export function KontrollPage() { const { objektId } = useParams<{ objektId: string }>(); const navigate = useNavigate(); @@ -27,6 +55,9 @@ export function KontrollPage() { nachfuellDialogSchliessen, } = useKontrolle(objektId); + const fachGruppen = gruppiereNachFach(positionen); + const nurOhneFach = fachGruppen.length === 1 && fachGruppen[0][0] === OHNE_FACH; + async function handleAbschliessen() { if (await abschliessen()) { navigate("/objekte"); @@ -93,28 +124,33 @@ export function KontrollPage() { Kein Netz – Eingaben werden lokal gespeichert und automatisch übertragen, sobald wieder Verbindung besteht. )} -
    - {positionen.map((zustand) => ( -
  • - - {nachfuellDialog?.materialId === zustand.position.material_id && ( - ( +
    + {!nurOhneFach &&

    {fach}

    } +
      + {gruppenPositionen.map((zustand) => ( +
    • + + {nachfuellDialog?.materialId === zustand.position.material_id && ( + jetztNachfuellen(nachfuellDialog.fehlbestandId, menge)} + onSchliessen={nachfuellDialogSchliessen} + /> )} - onNachfuellen={(menge) => jetztNachfuellen(nachfuellDialog.fehlbestandId, menge)} - onSchliessen={nachfuellDialogSchliessen} - /> - )} -
    • - ))} -
    +
  • + ))} +
+ + ))} {abschlussFehler &&
{abschlussFehler}
}
diff --git a/frontend/src/pages/kontrolle/types.ts b/frontend/src/pages/kontrolle/types.ts index 10b282a..6fcbce8 100644 --- a/frontend/src/pages/kontrolle/types.ts +++ b/frontend/src/pages/kontrolle/types.ts @@ -12,6 +12,10 @@ export interface PositionZustand { chargennummer: string; seriennummer: string; fehlerText: string | null; + // Fach kommt aus der Vorlagenposition (nur für Material aus der Vorlage + // gesetzt, Karte "feste Fächer-Liste je Objekttyp") - null = ohne Fach + // zugeordnet oder Zusatzposition ohne Vorlagenbezug. + fach: string | null; } export interface Sperrmeldung { diff --git a/frontend/src/pages/kontrolle/useKontrolle.ts b/frontend/src/pages/kontrolle/useKontrolle.ts index 14f4c01..8808cee 100644 --- a/frontend/src/pages/kontrolle/useKontrolle.ts +++ b/frontend/src/pages/kontrolle/useKontrolle.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { apiRequest, ApiError } from "../../api/client"; -import type { Kontrolle, Material, Objekt, Objektposition } from "../../api/types"; +import type { Beladungsvorlage, Kontrolle, Material, Objekt, Objektposition } from "../../api/types"; import { aufErgebnisWarten, enqueue, @@ -63,12 +63,16 @@ export function useKontrolle(objektId: string | undefined) { }, [objektId]); useEffect(() => { - if (!kontrolle) return; + if (!kontrolle || !objekt) return; Promise.all([ apiRequest(`/objekte/${kontrolle.objekt_id}/positionen`), apiRequest("/materialien"), - ]).then(([positionsListe, materialListe]) => { + objekt.vorlage_id ? apiRequest(`/vorlagen/${objekt.vorlage_id}`) : Promise.resolve(null), + ]).then(([positionsListe, materialListe, vorlage]) => { const materialById = new Map(materialListe.map((m) => [m.id, m])); + const fachByMaterialId = new Map( + (vorlage?.positionen ?? []).map((vp) => [vp.material_id, vp.fach]) + ); setPositionen( positionsListe .filter((p) => p.ist_status === "aktiv") @@ -81,10 +85,11 @@ export function useKontrolle(objektId: string | undefined) { chargennummer: p.chargennummer ?? "", seriennummer: p.seriennummer ?? "", fehlerText: null, + fach: fachByMaterialId.get(p.material_id) ?? null, })) ); }); - }, [kontrolle]); + }, [kontrolle, objekt]); // Reconciliation-Loop: gleicht lokalen Anzeigezustand mit der Offline-Queue ab. // Ein Eintrag, der aus der Queue verschwunden ist, wurde erfolgreich übertragen