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 +++++++++++++------
|
- 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}`;
|
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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user