diff --git a/src/components/auftraege/AuftragDialog.jsx b/src/components/auftraege/AuftragDialog.jsx
index 564cbc6..2bded8c 100644
--- a/src/components/auftraege/AuftragDialog.jsx
+++ b/src/components/auftraege/AuftragDialog.jsx
@@ -1,12 +1,275 @@
+import { useEffect, useRef, useState } from 'react'
+import { Button, Dialog } from '../ui'
+import { createAuftrag, getAuftragPdf, getSettings, patchAuftrag, sendAuftrag } from '../../lib/financeApi'
+import { alsPdfBlob } from '../../lib/auftragPdf'
+import {
+ SCHRITTE,
+ auftragBestaetigung,
+ auftragFehler,
+ auftragHinweise,
+ auftragZuFormular,
+ formularZuNutzlast,
+ heuteBerlin,
+ kundenartWirksam,
+ neueSperre,
+ neuesFormular,
+ pruefeSchritt,
+ sendeGate,
+ zahlungsplanOptionen,
+} from '../../lib/auftragView'
+import { SchrittAuftrag, SchrittProjekt, SchrittZahlung } from './AuftragSchritte'
+import AuftragVorschau from './AuftragVorschau'
+import './auftrag.css'
+
/**
- * Platzhalter von W0: Strang W2 ersetzt diese Datei vollständig (Dateiname, Default-Export und Props bleiben).
- *
- * Props (V8): open, onClose, onSaved(auftrag) (AuftragTeam mit mailStatus; nach Speichern oder Senden, danach onClose()),
- * kunde (kommt mit aktueller kundenart: der Container gleicht sie über kundeMitKundenart ab), projektId (vorausgewähltes
- * Projekt oder null = freier Titel), auftrag (Entwurf zum Bearbeiten oder null), konten (Projektkonten für die Projektwahl),
- * onShowOverview (Weg „Zur Übersicht“).
- * Die PDF-Vorschau öffnet openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id))).
+ * Dialog „Auftragsbestätigung“ in vier Schritten (Projekt, Auftrag, Zahlung und Termine, Vorschau). Beim Wechsel nach
+ * Schritt 4 wird der Entwurf angelegt bzw. aktualisiert und das PDF geholt (die Vorschau gibt es nur zu einem gespeicherten
+ * Entwurf; archiviert wird sie nicht). „Senden“ fragt vorher nach und ist gesperrt, solange Kundendaten fehlen; Hinweise
+ * (Steuernummer, „Vor Ort“ bei Privatperson) blockieren nie. Nach Speichern oder Senden ruft der Dialog onSaved(auftrag) und
+ * danach immer onClose(): der Container schließt, lädt neu und meldet. Nur Entwürfe lassen sich bearbeiten.
*/
export default function AuftragDialog({ open, onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview }) {
- return null
+ if (!open) return null
+ if (auftrag && auftrag.status !== 'entwurf') {
+ return (
+ Schließen}>
+
+ Nur Entwürfe lassen sich bearbeiten. Eine gesendete Auftragsbestätigung lässt sich nur über „Neue Fassung“ in der Liste ändern.
+
+
+ )
+ }
+ return (
+
+ )
+}
+
+// Ids der Felder (Task 6): wenige Felder haben eine abweichende id
+function feldId(feld, art) {
+ if (feld === 'art') return 'ab-art-festpreis'
+ if (feld === 'zahlungsart') return `ab-zahlungsart-${zahlungsplanOptionen(art)[0].value}`
+ return `ab-${feld}`
+}
+
+function AuftragForm({ onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview }) {
+ const [schritt, setSchritt] = useState(1)
+ const [form, setForm] = useState(() => (auftrag ? auftragZuFormular(auftrag) : neuesFormular({ projektId: projektId || '', konten, heute: heuteBerlin() })))
+ const [fehler, setFehler] = useState({})
+ const [meldung, setMeldung] = useState(null) // { text, zurUebersicht }
+ const [entwurf, setEntwurf] = useState(auftrag || null)
+ const [signatur, setSignatur] = useState('')
+ const [pdfUrl, setPdfUrl] = useState('')
+ const [phase, setPhase] = useState('') // '' | 'speichern' | 'pdf' | 'senden'
+ const [einstellungen, setEinstellungen] = useState(undefined) // undefined = lädt, null = nicht ladbar
+ const [fokus, setFokus] = useState(null) // { id }
+ const sperre = useRef(neueSperre()).current
+ const aliveRef = useRef(true)
+ const schmutzigRef = useRef(false) // es wurde gespeichert und noch nicht gemeldet
+ const urlRef = useRef('')
+
+ useEffect(() => {
+ aliveRef.current = true
+ getSettings()
+ .then((d) => {
+ if (aliveRef.current) setEinstellungen(d?.einstellungen ?? null)
+ })
+ .catch(() => {
+ if (aliveRef.current) setEinstellungen(null)
+ })
+ return () => {
+ aliveRef.current = false
+ if (urlRef.current) URL.revokeObjectURL(urlRef.current)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (fokus) document.getElementById(fokus.id)?.focus()
+ }, [fokus])
+
+ const hinweise = auftragHinweise({ kunde, form, einstellungen })
+ const gate = sendeGate(kunde, form.kundenart)
+ const privat = kundenartWirksam(kunde, form.kundenart) === 'privat'
+ const beschaeftigt = phase !== ''
+
+ function zeigeFehler(schrittNr, r, art) {
+ setSchritt(schrittNr)
+ setFehler(r.fehler)
+ setFokus({ id: feldId(r.erstes, art) })
+ }
+
+ function weiter() {
+ const r = pruefeSchritt(schritt, form, { kunde })
+ if (!r.ok) {
+ setFehler(r.fehler)
+ setFokus({ id: feldId(r.erstes, form.art) })
+ return
+ }
+ setFehler({})
+ if (schritt === 3) zurVorschau()
+ else setSchritt(schritt + 1)
+ }
+
+ function zurueck() {
+ setMeldung(null)
+ setSchritt(Math.max(1, schritt - 1))
+ }
+
+ function zurVorschau() {
+ const r = formularZuNutzlast(form, { kunde })
+ if (!r.ok) {
+ zeigeFehler(r.schritt, r, form.art)
+ return
+ }
+ setSchritt(4)
+ vorschauLaden(r.payload)
+ }
+
+ async function vorschauLaden(payload) {
+ const neu = JSON.stringify(payload)
+ if (neu === signatur && pdfUrl) return
+ if (sperre.belegt) return
+ await sperre.run(async () => {
+ if (urlRef.current) URL.revokeObjectURL(urlRef.current)
+ urlRef.current = ''
+ setPdfUrl('')
+ setPhase('speichern')
+ setMeldung(null)
+ try {
+ const data = entwurf ? await patchAuftrag(entwurf.id, payload) : await createAuftrag({ kundenId: kunde.$id, ...payload })
+ if (!aliveRef.current) return
+ setEntwurf(data.auftrag)
+ schmutzigRef.current = true
+ setSignatur(neu)
+ setPhase('pdf')
+ const blob = await getAuftragPdf(data.auftrag.id)
+ const url = URL.createObjectURL(alsPdfBlob(blob))
+ if (!aliveRef.current) {
+ URL.revokeObjectURL(url)
+ return
+ }
+ urlRef.current = url
+ setPdfUrl(url)
+ } catch (err) {
+ if (aliveRef.current) setMeldung(auftragFehler(err)) // ein schon gespeicherter Entwurf bleibt
+ } finally {
+ if (aliveRef.current) setPhase('')
+ }
+ })
+ }
+
+ function speichernUndSchliessen() {
+ schmutzigRef.current = false
+ if (entwurf) onSaved(entwurf)
+ onClose()
+ }
+
+ function schliessen() {
+ if (beschaeftigt) return
+ if (schmutzigRef.current && entwurf) onSaved(entwurf)
+ schmutzigRef.current = false
+ onClose()
+ }
+
+ async function senden() {
+ if (sperre.belegt || !entwurf) return
+ const g = sendeGate(kunde, form.kundenart)
+ if (!g.erlaubt) {
+ setMeldung({ text: g.grund, zurUebersicht: g.zurUebersicht })
+ return
+ }
+ if (!window.confirm(auftragBestaetigung('senden', entwurf, { email: kunde?.email }))) return
+ await sperre.run(async () => {
+ setPhase('senden')
+ setMeldung(null)
+ try {
+ const data = await sendAuftrag(entwurf.id)
+ schmutzigRef.current = false
+ onSaved(data.auftrag)
+ onClose()
+ } catch (err) {
+ if (aliveRef.current) setMeldung(auftragFehler(err))
+ } finally {
+ if (aliveRef.current) setPhase('')
+ }
+ })
+ }
+
+ const fuss = (
+
+ {schritt > 1 && (
+ Zurück
+ )}
+
+ {schritt < 4 ? (
+ <>
+ Abbrechen
+ {schritt === 3 ? 'Zur Vorschau' : 'Weiter'}
+ >
+ ) : (
+ <>
+ Als Entwurf speichern
+
+ {phase === 'senden' ? 'Sende …' : 'Senden'}
+
+ >
+ )}
+
+ )
+
+ return (
+ {
+ e.preventDefault()
+ if (schritt < 4) weiter()
+ }}
+ footer={fuss}
+ >
+
+ {SCHRITTE.map((s) => (
+
+ {s.id}
+ {s.label}
+
+ ))}
+
+ {schritt === 1 && }
+ {schritt === 2 && }
+ {schritt === 3 && }
+ {schritt === 4 && (
+ {
+ schliessen()
+ onShowOverview()
+ }}
+ />
+ )}
+
+ )
}
diff --git a/src/components/auftraege/AuftragVorschau.jsx b/src/components/auftraege/AuftragVorschau.jsx
new file mode 100644
index 0000000..28dd59a
--- /dev/null
+++ b/src/components/auftraege/AuftragVorschau.jsx
@@ -0,0 +1,80 @@
+import { useRef } from 'react'
+import { Button } from '../ui'
+import { useScrollIntoView } from '../../hooks/useScrollIntoView'
+import './auftrag.css'
+
+/**
+ * Schritt 4 des Dialogs als reine Darstellung (kein eigener Zustand): Lade- und Fehlerzustand, Hinweise und die PDF-Vorschau.
+ * Alles, was vor dem Senden zu lesen ist, steht über dem hohen PDF-Rahmen. Hinweise sind reine Texte und sperren nichts;
+ * nur `gate` (Kundendaten) hält das Senden auf, der Entwurf lässt sich trotzdem speichern.
+ *
+ * phase: '' | 'speichern' | 'pdf' | 'senden'; meldung: { text, zurUebersicht? } | null; gate: { erlaubt, grund, zurUebersicht };
+ * hinweise: [{ key, text }]; privat: Kundenart Privatperson.
+ */
+export default function AuftragVorschau({ phase, pdfUrl, meldung, gate, privat, hinweise = [], onRetry, onShowOverview }) {
+ const errorRef = useRef(null)
+ useScrollIntoView(errorRef, meldung)
+ const laedt = phase === 'speichern' || phase === 'pdf'
+
+ return (
+
+ {phase === 'speichern' &&
Entwurf wird gespeichert …
}
+ {phase === 'pdf' &&
Vorschau wird erstellt …
}
+
+ {meldung && (
+
+ {meldung.text}
+ {meldung.zurUebersicht && (
+ <>
+ {' '}
+
+ Zur Übersicht
+
+ >
+ )}
+
+ )}
+ {meldung && !pdfUrl && !laedt && phase !== 'senden' && (
+
+ Erneut versuchen
+
+ )}
+
+ {pdfUrl &&
Entwurf gespeichert. Er bleibt in der Liste, bis er gesendet oder gelöscht wird.
}
+
+ {gate && !gate.erlaubt && (
+
+ {gate.grund}
+ {gate.zurUebersicht && (
+ <>
+ {' '}
+
+ Zur Übersicht
+
+ >
+ )}
+
+ )}
+
+ {hinweise.map((h) => (
+
{h.text}
+ ))}
+
+ {privat && (
+
+ Privatperson: Die Auftragsbestätigung enthält die Widerrufsbelehrung. Mit der Arbeit darf erst nach Ablauf der Widerrufsfrist begonnen
+ werden, es sei denn, der Kunde verlangt es ausdrücklich (Häkchen auf der Bestätigungsseite).
+
+ )}
+
+ {pdfUrl && (
+ <>
+
+
+ PDF in neuem Tab öffnen
+
+ >
+ )}
+
+ )
+}
diff --git a/src/components/auftraege/auftrag.css b/src/components/auftraege/auftrag.css
index caeca2c..49a8c60 100644
--- a/src/components/auftraege/auftrag.css
+++ b/src/components/auftraege/auftrag.css
@@ -76,3 +76,18 @@
.auftrag-item + .auftrag-item .auftrag-item-label, .auftrag-item-label { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal; }
.auftrag-narrow { max-width: none; }
}
+
+/* Dialog: Schrittleiste, Vorschau, Fuss */
+.auftrag-schritte { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 20px; font-size: 13px; color: var(--text-2); }
+.auftrag-schritte li[aria-current="step"] { color: var(--text); font-weight: 600; }
+.auftrag-schritt-nr { margin-right: 6px; }
+.auftrag-pdf { width: 100%; height: min(70vh, 640px); border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-subtle); }
+.auftrag-foot { display: flex; flex: 1; flex-wrap: wrap; align-items: center; gap: 8px; }
+.auftrag-foot .grow { flex: 1; }
+
+@media (max-width: 860px) {
+ .auftrag-schritt-text { display: none; }
+ .auftrag-schritte li[aria-current="step"] .auftrag-schritt-text { display: inline; }
+ .auftrag-pdf { height: 50vh; }
+ .auftrag-foot .btn { flex: 1 1 auto; }
+}