Inline-Nachfüll-Dialog nach Abweichung in der Kontrolle (Karte 07)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -1986,3 +1986,68 @@ Keine Commits in dieser Session.
|
||||
- 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 +++-
|
||||
|
||||
---
|
||||
|
||||
@@ -31,6 +31,18 @@ function keyFor(kontrolleId: string, materialId: number): string {
|
||||
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(
|
||||
kontrolleId: string,
|
||||
materialId: number,
|
||||
@@ -88,7 +100,11 @@ async function sendeEintrag(item: QueueItem): Promise<void> {
|
||||
// Erfolgreich übertragen (idempotent - ein erneutes Senden desselben Werts
|
||||
// durch die Queue vor einem Absturz führt zu keinem doppelten Fehlbestand,
|
||||
// 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);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { NachfuellDialog } from "./kontrolle/NachfuellDialog";
|
||||
import { PositionCard } from "./kontrolle/PositionCard";
|
||||
import { useKontrolle } from "./kontrolle/useKontrolle";
|
||||
|
||||
@@ -15,12 +16,15 @@ export function KontrollPage() {
|
||||
wirdAbgeschlossen,
|
||||
abschlussFehler,
|
||||
alleUebertragen,
|
||||
nachfuellDialog,
|
||||
starteKontrolle,
|
||||
eingabeAendern,
|
||||
positionSenden,
|
||||
fehlerErneutVersuchen,
|
||||
abschliessen,
|
||||
abbrechen,
|
||||
jetztNachfuellen,
|
||||
nachfuellDialogSchliessen,
|
||||
} = useKontrolle(objektId);
|
||||
|
||||
async function handleAbschliessen() {
|
||||
@@ -91,13 +95,24 @@ export function KontrollPage() {
|
||||
)}
|
||||
<ul className="card-list" style={{ marginBottom: "1.25rem" }}>
|
||||
{positionen.map((zustand) => (
|
||||
<li key={zustand.position.id} style={{ listStyle: "none" }}>
|
||||
<PositionCard
|
||||
key={zustand.position.id}
|
||||
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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
return (
|
||||
<li className="card">
|
||||
<div className="card">
|
||||
<div className="card-title-row">
|
||||
<strong>{zustand.material?.name ?? `Material ${zustand.position.material_id}`}</strong>
|
||||
<StatusBadge status={zustand.status} />
|
||||
@@ -90,6 +90,6 @@ export function PositionCard({ zustand, onEingabeAendern, onSenden, onErneutVers
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { apiRequest, ApiError } from "../../api/client";
|
||||
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 type { PositionZustand, Sperrmeldung } from "./types";
|
||||
|
||||
@@ -22,6 +28,9 @@ export function useKontrolle(objektId: string | undefined) {
|
||||
const [startFehler, setStartFehler] = useState<string | null>(null);
|
||||
const [wirdAbgeschlossen, setWirdAbgeschlossen] = useState(false);
|
||||
const [abschlussFehler, setAbschlussFehler] = useState<string | null>(null);
|
||||
const [nachfuellDialog, setNachfuellDialog] = useState<{ materialId: number; fehlbestandId: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!objektId) return;
|
||||
@@ -113,6 +122,11 @@ export function useKontrolle(objektId: string | undefined) {
|
||||
|
||||
async function positionSenden(materialId: number, zustand: PositionZustand) {
|
||||
if (!kontrolle) return;
|
||||
aufErgebnisWarten(kontrolle.id, materialId, (fehlbestandId, abweichung) => {
|
||||
if (abweichung && fehlbestandId) {
|
||||
setNachfuellDialog({ materialId, fehlbestandId });
|
||||
}
|
||||
});
|
||||
await enqueue(kontrolle.id, materialId, zustand.eingabe, {
|
||||
ablaufdatum: zustand.ablaufdatum || null,
|
||||
chargennummer: zustand.chargennummer || null,
|
||||
@@ -124,6 +138,19 @@ export function useKontrolle(objektId: string | undefined) {
|
||||
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) {
|
||||
if (!kontrolle) return;
|
||||
await entferneFehlerEintrag(kontrolle.id, materialId);
|
||||
@@ -168,11 +195,14 @@ export function useKontrolle(objektId: string | undefined) {
|
||||
wirdAbgeschlossen,
|
||||
abschlussFehler,
|
||||
alleUebertragen,
|
||||
nachfuellDialog,
|
||||
starteKontrolle,
|
||||
eingabeAendern,
|
||||
positionSenden,
|
||||
fehlerErneutVersuchen,
|
||||
abschliessen,
|
||||
abbrechen,
|
||||
jetztNachfuellen,
|
||||
nachfuellDialogSchliessen,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user