Inline-Nachfüll-Dialog nach Abweichung in der Kontrolle (Karte 07)
CI / backend-tests (push) Successful in 54s
CI / frontend-build (push) Successful in 7m17s

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-04 09:04:20 +02:00
co-authored by Claude Sonnet 5
parent db245c5e43
commit d70558f124
6 changed files with 173 additions and 11 deletions
+65
View File
@@ -1986,3 +1986,68 @@ Keine Commits in dieser Session.
- frontend/src/pages/kontrolle/useKontrolle.ts | 19 +++++++++++++------ - frontend/src/pages/kontrolle/useKontrolle.ts | 19 +++++++++++++------
--- ---
## 2026-09-04 08:53 08:54 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- db245c5 PWA: Passt-Button, Fortschrittsanzeige, Ablaufdatum-Farbcodierung in Kontrolle
### Geänderte Dateien
- DEVLOG.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/pages/KontrollPage.tsx | 19 +++++
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
---
## 2026-09-04 08:54 08:55 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/pages/KontrollPage.tsx | 19 +++++
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
---
## 2026-09-04 08:55 08:55 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/pages/KontrollPage.tsx | 19 +++++
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
---
## 2026-09-04 08:55 08:59 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/pages/KontrollPage.tsx | 19 +++++
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
---
## 2026-09-04 08:55 08:59 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/pages/KontrollPage.tsx | 19 +++++
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
---
+17 -1
View File
@@ -31,6 +31,18 @@ function keyFor(kontrolleId: string, materialId: number): string {
return `${kontrolleId}:${materialId}`; return `${kontrolleId}:${materialId}`;
} }
// Karte 07 (Sofort-Nachfüllung): die PWA soll direkt nach einer Abweichung
// fragen können "jetzt nachfüllen?", ohne ins Dashboard zu wechseln. Der
// Queue-Eintrag verschwindet bei Erfolg (siehe sendeEintrag), daher hier ein
// einmaliger Callback pro Key statt dauerhaftem State - reicht für den
// unmittelbaren Dialog in derselben Sitzung.
type ErgebnisListener = (fehlbestandId: string | null, abweichung: boolean) => void;
const ergebnisListener = new Map<string, ErgebnisListener>();
export function aufErgebnisWarten(kontrolleId: string, materialId: number, listener: ErgebnisListener): void {
ergebnisListener.set(keyFor(kontrolleId, materialId), listener);
}
export async function enqueue( export async function enqueue(
kontrolleId: string, kontrolleId: string,
materialId: number, materialId: number,
@@ -88,7 +100,11 @@ async function sendeEintrag(item: QueueItem): Promise<void> {
// Erfolgreich übertragen (idempotent - ein erneutes Senden desselben Werts // Erfolgreich übertragen (idempotent - ein erneutes Senden desselben Werts
// durch die Queue vor einem Absturz führt zu keinem doppelten Fehlbestand, // durch die Queue vor einem Absturz führt zu keinem doppelten Fehlbestand,
// siehe Backend app/services/kontrolle.py erfasse_position) -> aus Queue entfernen. // siehe Backend app/services/kontrolle.py erfasse_position) -> aus Queue entfernen.
void ergebnis; const listener = ergebnisListener.get(item.key);
if (listener) {
listener(ergebnis.fehlbestand_id, ergebnis.abweichung);
ergebnisListener.delete(item.key);
}
await del(item.key, store); await del(item.key, store);
} catch (error) { } catch (error) {
if (error instanceof ApiError && error.status >= 400 && error.status < 500) { if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
+22 -7
View File
@@ -1,5 +1,6 @@
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { NachfuellDialog } from "./kontrolle/NachfuellDialog";
import { PositionCard } from "./kontrolle/PositionCard"; import { PositionCard } from "./kontrolle/PositionCard";
import { useKontrolle } from "./kontrolle/useKontrolle"; import { useKontrolle } from "./kontrolle/useKontrolle";
@@ -15,12 +16,15 @@ export function KontrollPage() {
wirdAbgeschlossen, wirdAbgeschlossen,
abschlussFehler, abschlussFehler,
alleUebertragen, alleUebertragen,
nachfuellDialog,
starteKontrolle, starteKontrolle,
eingabeAendern, eingabeAendern,
positionSenden, positionSenden,
fehlerErneutVersuchen, fehlerErneutVersuchen,
abschliessen, abschliessen,
abbrechen, abbrechen,
jetztNachfuellen,
nachfuellDialogSchliessen,
} = useKontrolle(objektId); } = useKontrolle(objektId);
async function handleAbschliessen() { async function handleAbschliessen() {
@@ -91,13 +95,24 @@ export function KontrollPage() {
)} )}
<ul className="card-list" style={{ marginBottom: "1.25rem" }}> <ul className="card-list" style={{ marginBottom: "1.25rem" }}>
{positionen.map((zustand) => ( {positionen.map((zustand) => (
<PositionCard <li key={zustand.position.id} style={{ listStyle: "none" }}>
key={zustand.position.id} <PositionCard
zustand={zustand} zustand={zustand}
onEingabeAendern={eingabeAendern} onEingabeAendern={eingabeAendern}
onSenden={positionSenden} onSenden={positionSenden}
onErneutVersuchen={fehlerErneutVersuchen} 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> </ul>
@@ -0,0 +1,36 @@
import { useState } from "react";
interface Props {
materialName: string;
vorschlagMenge: string;
onNachfuellen: (menge: string) => void;
onSchliessen: () => void;
}
/** Karte 07: nach einer Abweichung direkt "jetzt nachfüllen?" statt Umweg über Dashboard. */
export function NachfuellDialog({ materialName, vorschlagMenge, onNachfuellen, onSchliessen }: Props) {
const [menge, setMenge] = useState(vorschlagMenge);
return (
<div className="card" style={{ borderColor: "var(--color-warning, #b45309)", marginTop: "0.6rem" }}>
<div className="card-subtitle">
Fehlmenge bei {materialName} jetzt aus Lager nachfüllen?
</div>
<div className="row" style={{ marginTop: "0.5rem" }}>
<input
type="number"
className="input"
value={menge}
onChange={(e) => setMenge(e.target.value)}
style={{ width: "6rem" }}
/>
<button className="btn btn-primary" onClick={() => onNachfuellen(menge)}>
Ja, nachfüllen
</button>
<button className="btn btn-secondary" onClick={onSchliessen}>
Nein
</button>
</div>
</div>
);
}
@@ -26,7 +26,7 @@ export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVers
const passt = zustand.eingabe === zustand.position.sollmenge_effektiv; const passt = zustand.eingabe === zustand.position.sollmenge_effektiv;
return ( return (
<li className="card"> <div className="card">
<div className="card-title-row"> <div className="card-title-row">
<strong>{zustand.material?.name ?? `Material ${zustand.position.material_id}`}</strong> <strong>{zustand.material?.name ?? `Material ${zustand.position.material_id}`}</strong>
<StatusBadge status={zustand.status} /> <StatusBadge status={zustand.status} />
@@ -90,6 +90,6 @@ export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVers
</button> </button>
</div> </div>
)} )}
</li> </div>
); );
} }
+31 -1
View File
@@ -2,7 +2,13 @@ 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 { Kontrolle, Material, Objekt, Objektposition } from "../../api/types";
import { enqueue, eintraegeFuerKontrolle, entferneFehlerEintrag, synchronisiereQueue } from "../../offline/queue"; import {
aufErgebnisWarten,
enqueue,
eintraegeFuerKontrolle,
entferneFehlerEintrag,
synchronisiereQueue,
} from "../../offline/queue";
import { useOnlineStatus } from "../../offline/useOnlineStatus"; import { useOnlineStatus } from "../../offline/useOnlineStatus";
import type { PositionZustand, Sperrmeldung } from "./types"; import type { PositionZustand, Sperrmeldung } from "./types";
@@ -22,6 +28,9 @@ export function useKontrolle(objektId: string | undefined) {
const [startFehler, setStartFehler] = useState<string | null>(null); const [startFehler, setStartFehler] = useState<string | null>(null);
const [wirdAbgeschlossen, setWirdAbgeschlossen] = useState(false); const [wirdAbgeschlossen, setWirdAbgeschlossen] = useState(false);
const [abschlussFehler, setAbschlussFehler] = useState<string | null>(null); const [abschlussFehler, setAbschlussFehler] = useState<string | null>(null);
const [nachfuellDialog, setNachfuellDialog] = useState<{ materialId: number; fehlbestandId: string } | null>(
null
);
useEffect(() => { useEffect(() => {
if (!objektId) return; if (!objektId) return;
@@ -113,6 +122,11 @@ export function useKontrolle(objektId: string | undefined) {
async function positionSenden(materialId: number, zustand: PositionZustand) { async function positionSenden(materialId: number, zustand: PositionZustand) {
if (!kontrolle) return; if (!kontrolle) return;
aufErgebnisWarten(kontrolle.id, materialId, (fehlbestandId, abweichung) => {
if (abweichung && fehlbestandId) {
setNachfuellDialog({ materialId, fehlbestandId });
}
});
await enqueue(kontrolle.id, materialId, zustand.eingabe, { await enqueue(kontrolle.id, materialId, zustand.eingabe, {
ablaufdatum: zustand.ablaufdatum || null, ablaufdatum: zustand.ablaufdatum || null,
chargennummer: zustand.chargennummer || null, chargennummer: zustand.chargennummer || null,
@@ -124,6 +138,19 @@ export function useKontrolle(objektId: string | undefined) {
void synchronisiereQueue(); void synchronisiereQueue();
} }
/** Karte 07: Sofort-Nachfüllung direkt aus dem Dialog nach einer Abweichung. */
async function jetztNachfuellen(fehlbestandId: string, menge: string) {
await apiRequest(`/fehlbestaende/${fehlbestandId}/nachfuellungen`, {
method: "POST",
body: { menge },
});
setNachfuellDialog(null);
}
function nachfuellDialogSchliessen() {
setNachfuellDialog(null);
}
async function fehlerErneutVersuchen(materialId: number, zustand: PositionZustand) { async function fehlerErneutVersuchen(materialId: number, zustand: PositionZustand) {
if (!kontrolle) return; if (!kontrolle) return;
await entferneFehlerEintrag(kontrolle.id, materialId); await entferneFehlerEintrag(kontrolle.id, materialId);
@@ -168,11 +195,14 @@ export function useKontrolle(objektId: string | undefined) {
wirdAbgeschlossen, wirdAbgeschlossen,
abschlussFehler, abschlussFehler,
alleUebertragen, alleUebertragen,
nachfuellDialog,
starteKontrolle, starteKontrolle,
eingabeAendern, eingabeAendern,
positionSenden, positionSenden,
fehlerErneutVersuchen, fehlerErneutVersuchen,
abschliessen, abschliessen,
abbrechen, abbrechen,
jetztNachfuellen,
nachfuellDialogSchliessen,
}; };
} }