Kontroll-Erfassung: Positionen nach Fach gruppieren
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -3028,3 +3028,33 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/styles/global.css | 19 ++++++
|
- 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 +++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -116,3 +116,18 @@ Keine Commits in dieser Session.
|
|||||||
- ...07_indizes_und_unique_constraints.py => 0007_indizes_und_locks.py} | 4 ++--
|
- ...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 +++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ export const TEST_POSITION = {
|
|||||||
chargennummer: null,
|
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 = {
|
export const TEST_KONTROLLE = {
|
||||||
id: "kontrolle-1",
|
id: "kontrolle-1",
|
||||||
objekt_id: 1,
|
objekt_id: 1,
|
||||||
@@ -88,6 +97,7 @@ export async function mockKontrollGrundgeruest(page: Page): Promise<void> {
|
|||||||
route.fulfill({ json: [TEST_POSITION] })
|
route.fulfill({ json: [TEST_POSITION] })
|
||||||
);
|
);
|
||||||
await page.route("**/api/v1/materialien", (route) => route.fulfill({ json: [TEST_MATERIAL] }));
|
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<void> {
|
export async function login(page: Page): Promise<void> {
|
||||||
|
|||||||
@@ -2,8 +2,36 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
import { NachfuellDialog } from "./kontrolle/NachfuellDialog";
|
import { NachfuellDialog } from "./kontrolle/NachfuellDialog";
|
||||||
import { PositionCard } from "./kontrolle/PositionCard";
|
import { PositionCard } from "./kontrolle/PositionCard";
|
||||||
|
import type { PositionZustand } from "./kontrolle/types";
|
||||||
import { useKontrolle } from "./kontrolle/useKontrolle";
|
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<string, PositionZustand[]>();
|
||||||
|
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() {
|
export function KontrollPage() {
|
||||||
const { objektId } = useParams<{ objektId: string }>();
|
const { objektId } = useParams<{ objektId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -27,6 +55,9 @@ export function KontrollPage() {
|
|||||||
nachfuellDialogSchliessen,
|
nachfuellDialogSchliessen,
|
||||||
} = useKontrolle(objektId);
|
} = useKontrolle(objektId);
|
||||||
|
|
||||||
|
const fachGruppen = gruppiereNachFach(positionen);
|
||||||
|
const nurOhneFach = fachGruppen.length === 1 && fachGruppen[0][0] === OHNE_FACH;
|
||||||
|
|
||||||
async function handleAbschliessen() {
|
async function handleAbschliessen() {
|
||||||
if (await abschliessen()) {
|
if (await abschliessen()) {
|
||||||
navigate("/objekte");
|
navigate("/objekte");
|
||||||
@@ -93,8 +124,11 @@ export function KontrollPage() {
|
|||||||
Kein Netz – Eingaben werden lokal gespeichert und automatisch übertragen, sobald wieder Verbindung besteht.
|
Kein Netz – Eingaben werden lokal gespeichert und automatisch übertragen, sobald wieder Verbindung besteht.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="card-list" style={{ marginBottom: "1.25rem" }}>
|
{fachGruppen.map(([fach, gruppenPositionen]) => (
|
||||||
{positionen.map((zustand) => (
|
<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" }}>
|
<li key={zustand.position.id} style={{ listStyle: "none" }}>
|
||||||
<PositionCard
|
<PositionCard
|
||||||
zustand={zustand}
|
zustand={zustand}
|
||||||
@@ -115,6 +149,8 @@ export function KontrollPage() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{abschlussFehler && <div className="alert alert-danger">{abschlussFehler}</div>}
|
{abschlussFehler && <div className="alert alert-danger">{abschlussFehler}</div>}
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export interface PositionZustand {
|
|||||||
chargennummer: string;
|
chargennummer: string;
|
||||||
seriennummer: string;
|
seriennummer: string;
|
||||||
fehlerText: string | null;
|
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 {
|
export interface Sperrmeldung {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { apiRequest, ApiError } from "../../api/client";
|
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 {
|
import {
|
||||||
aufErgebnisWarten,
|
aufErgebnisWarten,
|
||||||
enqueue,
|
enqueue,
|
||||||
@@ -63,12 +63,16 @@ export function useKontrolle(objektId: string | undefined) {
|
|||||||
}, [objektId]);
|
}, [objektId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kontrolle) return;
|
if (!kontrolle || !objekt) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
apiRequest<Objektposition[]>(`/objekte/${kontrolle.objekt_id}/positionen`),
|
apiRequest<Objektposition[]>(`/objekte/${kontrolle.objekt_id}/positionen`),
|
||||||
apiRequest<Material[]>("/materialien"),
|
apiRequest<Material[]>("/materialien"),
|
||||||
]).then(([positionsListe, materialListe]) => {
|
objekt.vorlage_id ? apiRequest<Beladungsvorlage>(`/vorlagen/${objekt.vorlage_id}`) : Promise.resolve(null),
|
||||||
|
]).then(([positionsListe, materialListe, vorlage]) => {
|
||||||
const materialById = new Map(materialListe.map((m) => [m.id, m]));
|
const materialById = new Map(materialListe.map((m) => [m.id, m]));
|
||||||
|
const fachByMaterialId = new Map(
|
||||||
|
(vorlage?.positionen ?? []).map((vp) => [vp.material_id, vp.fach])
|
||||||
|
);
|
||||||
setPositionen(
|
setPositionen(
|
||||||
positionsListe
|
positionsListe
|
||||||
.filter((p) => p.ist_status === "aktiv")
|
.filter((p) => p.ist_status === "aktiv")
|
||||||
@@ -81,10 +85,11 @@ export function useKontrolle(objektId: string | undefined) {
|
|||||||
chargennummer: p.chargennummer ?? "",
|
chargennummer: p.chargennummer ?? "",
|
||||||
seriennummer: p.seriennummer ?? "",
|
seriennummer: p.seriennummer ?? "",
|
||||||
fehlerText: null,
|
fehlerText: null,
|
||||||
|
fach: fachByMaterialId.get(p.material_id) ?? null,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [kontrolle]);
|
}, [kontrolle, objekt]);
|
||||||
|
|
||||||
// Reconciliation-Loop: gleicht lokalen Anzeigezustand mit der Offline-Queue ab.
|
// Reconciliation-Loop: gleicht lokalen Anzeigezustand mit der Offline-Queue ab.
|
||||||
// Ein Eintrag, der aus der Queue verschwunden ist, wurde erfolgreich übertragen
|
// Ein Eintrag, der aus der Queue verschwunden ist, wurde erfolgreich übertragen
|
||||||
|
|||||||
Reference in New Issue
Block a user