From 3650573445974d200e7dec9300b343bb736beb3f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:34:47 +0000 Subject: [PATCH] Auftragsbestaetigung: Liste mit Status, Zusatzzeilen, Aktionen und Projekt zuordnen Co-Authored-By: Claude Sonnet 5.5 --- src/components/auftraege/AuftragListe.jsx | 247 +++++++++++++++++++++- src/components/auftraege/auftrag.css | 35 +++ 2 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 src/components/auftraege/auftrag.css diff --git a/src/components/auftraege/AuftragListe.jsx b/src/components/auftraege/AuftragListe.jsx index 3525a91..d66ba50 100644 --- a/src/components/auftraege/AuftragListe.jsx +++ b/src/components/auftraege/AuftragListe.jsx @@ -1,11 +1,244 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { Badge, Button, Card, EmptyState } from '../ui' +import { noticeProps } from '../../lib/financeUi' +import { + deleteAuftrag, + erinnereAuftrag, + getAuftragPdf, + neueFassungAuftrag, + ordneAuftragProjektZu, + sendAuftrag, + storniereAuftrag, +} from '../../lib/financeApi' +import { oeffnePdfImTab } from '../../lib/auftragPdf' +import { + auftragBestaetigung, + auftragErfolg, + auftragFehler, + auftragZeilen, + neueSperre, + zuordnenOptionen, +} from '../../lib/auftragView' +import './auftrag.css' + +const LAUFEND = { senden: 'Sende …', erinnern: 'Sende …', stornieren: 'Storniere …', loeschen: 'Lösche …', neueFassung: 'Lege an …', pdf: 'Öffne …' } +const AUFRUFE = { + senden: sendAuftrag, + erinnern: erinnereAuftrag, + stornieren: storniereAuftrag, + loeschen: deleteAuftrag, + neueFassung: neueFassungAuftrag, +} + /** - * Platzhalter von W0: Strang W2 ersetzt diese Datei vollständig (Dateiname, Default-Export und Props bleiben). - * Die fertige Komponente rendert selbst und ruft nach jeder Aktion onChanged(). - * - * Props (V8): auftraege (AuftragTeam[], V3), konten (Projektname der Zeile und Auswahl für „Projekt zuordnen“, einer Aktion für - * ABs ohne Projekt über ordneAuftragProjektZu(id, projektId) aus financeApi.js), onChanged(), onBearbeiten(auftrag) - * („Entwurf bearbeiten“; nach „Neue Fassung“ mit dem neuen Entwurf) und onShowOverview (Weg „Zur Übersicht“ aus Meldungen). + * Karte „Aufträge“ im Kundenreiter: Auftragsbestätigungen (AB) mit Status, Zusatzzeilen und den Aktionen je Status. + * Die Regeln stecken in src/lib/auftragView.js. Nach jeder Aktion ruft die Liste onChanged() (auch nach einem Fehler: + * der Stand kann ein anderer sein). „Bearbeiten“ und „Neue Fassung“ geben den Entwurf über onBearbeiten an den Container, + * „Zur Übersicht“ in Meldungen läuft über onShowOverview. Eine Sperre fängt den Doppelklick ab (eine Anfrage, eine Rückfrage). + * Schmal (Wrapper .auftrag-body bis 860 px) wird jede Zeile ein Raster. */ export default function AuftragListe({ auftraege, konten, onChanged, onBearbeiten, onShowOverview }) { - return null + const zeilen = useMemo(() => auftragZeilen(auftraege, { konten }), [auftraege, konten]) + const optionen = useMemo(() => zuordnenOptionen(konten), [konten]) + const [busy, setBusy] = useState(null) // `${id}:${aktion}` der laufenden Anfrage + const [message, setMessage] = useState(null) // { tone, text, zurUebersicht? } + const [zuordnen, setZuordnen] = useState(null) // { id, projektId } der Zeile mit offener Auswahl + const sperre = useRef(neueSperre()).current + const aliveRef = useRef(true) + + useEffect(() => { + aliveRef.current = true + return () => { + aliveRef.current = false + } + }, []) + + const zeigeFehler = (err) => { + const f = auftragFehler(err) + if (aliveRef.current) setMessage({ tone: 'warn', text: f.text, zurUebersicht: f.zurUebersicht }) + } + + async function run(zeile, aktion) { + if (aktion.key === 'bearbeiten') { + onBearbeiten(zeile.auftrag) + return + } + if (aktion.key === 'projekt') { + setZuordnen({ id: zeile.id, projektId: '' }) + setMessage(null) + return + } + if (sperre.belegt) return + const frage = auftragBestaetigung(aktion.key, zeile.auftrag) + if (frage && !window.confirm(frage)) return + await sperre.run(async () => { + setBusy(`${zeile.id}:${aktion.key}`) + setMessage(null) + try { + if (aktion.key === 'pdf') { + // kein await davor: der neue Tab muss im Klick aufgehen + await oeffnePdfImTab(() => getAuftragPdf(zeile.id)) + return + } + const data = await AUFRUFE[aktion.key](zeile.id) + if (aliveRef.current) setMessage(auftragErfolg(aktion.key, data?.auftrag || zeile.auftrag, { mailFehler: data?.mailFehler })) + onChanged() + if (aktion.key === 'neueFassung' && data?.auftrag) onBearbeiten(data.auftrag) + } catch (err) { + if (aktion.key === 'pdf') { + if (aliveRef.current) setMessage({ tone: 'warn', text: auftragFehler(err).text }) + return + } + zeigeFehler(err) + onChanged() // der Stand kann ein anderer sein + } finally { + if (aliveRef.current) setBusy(null) + } + }) + } + + async function ordneZu(zeile) { + const gewaehlt = optionen.find((o) => o.value === zuordnen?.projektId) + if (!gewaehlt || sperre.belegt) return + const frage = auftragBestaetigung('projekt', zeile.auftrag, { projektName: gewaehlt.label }) + if (frage && !window.confirm(frage)) return + await sperre.run(async () => { + setBusy(`${zeile.id}:projekt`) + setMessage(null) + try { + const data = await ordneAuftragProjektZu(zeile.id, gewaehlt.value) + if (aliveRef.current) { + setMessage(auftragErfolg('projekt', data?.auftrag || zeile.auftrag, { projektName: gewaehlt.label })) + setZuordnen(null) + } + onChanged() + } catch (err) { + zeigeFehler(err) + if (aliveRef.current) setZuordnen(null) + onChanged() + } finally { + if (aliveRef.current) setBusy(null) + } + }) + } + + return ( + +
+ {message && ( +

+ {message.text} + {message.zurUebersicht && ( + <> + {' '} + + + )} +

+ )} + + {zeilen.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + + + + + {zeilen.map((zeile) => { + const offen = zuordnen?.id === zeile.id + const wahl = offen && optionen.some((o) => o.value === zuordnen.projektId) ? zuordnen.projektId : '' + const ordnet = busy === `${zeile.id}:projekt` + return ( + + + + + + + + + ) + })} + +
Auftragsbestätigungen, neueste zuerst
NummerProjekt / TitelDatumSummeStatusAktionen
+ {zeile.istEntwurf ? {zeile.nummerText} : zeile.nummerText} + + {zeile.titel} + {zeile.projektText &&
{zeile.projektText}
} +
{zeile.datumText} + {zeile.summeText} + {zeile.summeSub &&
{zeile.summeSub}
} +
+ {zeile.statusLabel} + {zeile.notizen.map((note) => ( +
+ {note.text} +
+ ))} +
+
+ {zeile.aktionen.map((aktion) => { + const label = `${aktion.label}: ${zeile.nummerText}, ${zeile.titel}` + const text = busy === `${zeile.id}:${aktion.key}` && LAUFEND[aktion.key] ? LAUFEND[aktion.key] : aktion.label + if (aktion.danger) { + return ( + + ) + } + return ( + + ) + })} +
+ {offen && ( +
+ + + +
+ )} +
+
+ )} +
+
+ ) } diff --git a/src/components/auftraege/auftrag.css b/src/components/auftraege/auftrag.css new file mode 100644 index 0000000..62dbd56 --- /dev/null +++ b/src/components/auftraege/auftrag.css @@ -0,0 +1,35 @@ +/* Auftragsbestaetigungen (W2): Liste, Dialog-Schritte, Vorschau. Nur Tokens, alle Klassen beginnen mit auftrag-. + Die Datei wird von der Komponente geladen, global.css kommt danach: darum nie eine globale Klasse ueberschreiben. */ + +/* Liste */ +.auftrag-body { container-type: inline-size; } +.auftrag-notice { margin: 0 0 8px; } +.auftrag-table-wrap { overflow-x: auto; } +.auftrag-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } +.auftrag-nummer { white-space: nowrap; } +.auftrag-titel { overflow-wrap: anywhere; } +.auftrag-table th.auftrag-amount { text-align: right; } +.auftrag-amount { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; } +.auftrag-note { margin-top: 2px; font-size: 13px; overflow-wrap: anywhere; } +.auftrag-actions { display: flex; flex-wrap: wrap; gap: 4px 14px; } +.auftrag-zuordnen { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 8px; } +.auftrag-zuordnen .select { flex: 1 1 180px; min-width: 0; max-width: 100%; } +.auftrag-liste button.link:disabled, .auftrag-liste button.link-danger:disabled { cursor: not-allowed; } + +@container (max-width: 860px) { + .auftrag-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + .auftrag-table, .auftrag-table tbody { display: block; } + .auftrag-table tr { + display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; align-items: baseline; + grid-template-areas: "nummer status" "titel titel" "datum summe" "aktionen aktionen"; + padding: 10px 0; border-bottom: 1px solid var(--line); + } + .auftrag-table td, .auftrag-table td:first-child, .auftrag-table td:last-child { display: block; padding: 0; border: 0; min-width: 0; } + .auftrag-nummer { grid-area: nummer; } + .auftrag-status { grid-area: status; text-align: right; } + .auftrag-titel { grid-area: titel; } + .auftrag-datum { grid-area: datum; } + .auftrag-table td.auftrag-amount { grid-area: summe; } + .auftrag-aktionen { grid-area: aktionen; padding-top: 4px; } + .auftrag-table td[data-label]::before { content: attr(data-label) " "; color: var(--text-2); font-weight: 500; } +}