diff --git a/src/components/auftraege/RechnungDialog.jsx b/src/components/auftraege/RechnungDialog.jsx
index cfdd282..dc8d40c 100644
--- a/src/components/auftraege/RechnungDialog.jsx
+++ b/src/components/auftraege/RechnungDialog.jsx
@@ -55,7 +55,7 @@ import {
import { anfangszustand, reduce } from '../../lib/rechnungDialogState'
/**
- * Dialog „Rechnung schreiben“ (ersetzt InvoiceCreateModal). Ablauf von oben nach unten: Kunde (fest oder per Suche), Projekt,
+ * Dialog „Rechnung schreiben“. Ablauf von oben nach unten: Kunde (fest oder per Suche), Projekt,
* Projektkonto (Vereinbart, Berechnet, Noch offen, Zeit), Art der Rechnung (Anzahlung, Schlussrechnung, nach Aufwand, Festbetrag,
* ohne Projekt nur freie Positionen), Inhalt je Art mit Vorschau vom Server, Leistungszeitraum, Zahlungsziel und Notiz.
* „Als Entwurf speichern“ und „Senden“ bleiben gesperrt, solange die Vorschau nicht zur aktuellen Eingabe gehört.
diff --git a/src/components/finance/InvoiceCreateModal.jsx b/src/components/finance/InvoiceCreateModal.jsx
deleted file mode 100644
index 44ecf91..0000000
--- a/src/components/finance/InvoiceCreateModal.jsx
+++ /dev/null
@@ -1,320 +0,0 @@
-import { useEffect, useMemo, useRef, useState } from 'react'
-import { AutocompleteInput, Button, Dialog, Field } from '../ui'
-import AddressWarning from './AddressWarning'
-import { useCustomers } from '../../hooks/useCustomers'
-import { useScrollIntoView } from '../../hooks/useScrollIntoView'
-import { createInvoice } from '../../lib/financeApi'
-import { canInvoice, euro } from '../../lib/financeFormat'
-import {
- ADDRESS_INCOMPLETE_TEXT,
- INVOICE_LIMITS,
- customerName,
- customerPickerOptions,
- customerSuggestions,
- finalizedFilter,
- invoiceFailureMessage,
- invoiceSendConfirmText,
- invoiceSendGate,
- invoiceTotalCents,
- isFinalizedNotSent,
- lineTotalCents,
- resolveCustomerText,
- validateInvoiceForm,
-} from '../../lib/financeView'
-
-let itemCounter = 0
-const newItem = () => ({ uid: ++itemCounter, description: '', quantity: '1', price: '' })
-
-/**
- * Dialog „Rechnung schreiben“. Kunde (nur feste Kunden mit E-Mail wählbar; mit `fixedCustomer` steht er fest),
- * Positionen (Beschreibung, Menge, Einzelpreis in Euro), Zahlungsziel (Standard 14 Tage), Notiz, Summe und der
- * Hinweis nach § 19 UStG. „Als Entwurf speichern“ und „Senden“ sperren sich gegenseitig, solange die Anfrage läuft;
- * Fehler des Servers stehen im Dialog (und werden in den sichtbaren Bereich geholt). „Senden“ fragt vorher nach
- * (die Rechnung wird festgeschrieben und per Mail an den Kunden geschickt). Fehlt dem Kunden die Anschrift (Straße,
- * PLZ, Ort), steht eine Warnung mit Weg zur Übersicht da, und „Senden“ ist über 250 € gesperrt (§ 14 Abs. 4 Nr. 1
- * UStG; Entwürfe bleiben möglich). `onShowOverview` (Reiter am Kunden) wechselt zur Übersicht, sonst führt ein Link
- * zur Kundenseite.
- *
- * onCreated(rechnung, { senden }) nach Erfolg; wurde die Rechnung festgeschrieben, aber nicht versendet (502 mit
- * invoiceId), kommt onCreated(null, { senden: true, notice, tone: 'warn', finalizedFilter }) und der Dialog schließt
- * (die Rechnung steht dann „offen“ in der Liste und lässt sich dort erneut senden). Danach ruft der Dialog onClose().
- */
-export default function InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer = null, onShowOverview = null }) {
- if (!open) return null
- // Die Kundenliste (Appwrite, bis 5000 Dokumente) wird nur geholt, wenn der Kunde nicht feststeht
- return fixedCustomer ? (
-
- ) : (
-
- )
-}
-
-function PickerForm({ onClose, onCreated }) {
- const { customers, loading, error } = useCustomers()
- return
-}
-
-function InvoiceForm({ onClose, onCreated, fixedCustomer, source, onShowOverview }) {
- const [customerText, setCustomerText] = useState('')
- const [items, setItems] = useState(() => [newItem()])
- const [days, setDays] = useState(String(INVOICE_LIMITS.defaultDays))
- const [memo, setMemo] = useState('')
- const [error, setError] = useState('')
- const [busy, setBusy] = useState(null) // 'draft' | 'send'
- const lockRef = useRef(false)
- const aliveRef = useRef(true)
- const errorRef = useRef(null)
-
- // Der Dialog kann höher sein als das Fenster: eine Fehlermeldung oben soll nicht außerhalb des Bildes stehen
- useScrollIntoView(errorRef, error)
-
- useEffect(() => {
- aliveRef.current = true
- return () => {
- aliveRef.current = false
- }
- }, [])
-
- // × und Esc kommen vom Dialog (er ignoriert Esc, wenn die Kundenliste die Taste schon behandelt hat, und solange `busy` gilt).
- // Die Sperre deckt den Augenblick zwischen Klick und erstem Zeichnen von `busy` ab.
- const close = () => {
- if (lockRef.current) return
- onClose()
- }
-
- const options = useMemo(() => customerPickerOptions(source?.customers), [source?.customers])
- const resolved = fixedCustomer
- ? { state: canInvoice(fixedCustomer).ok ? 'ok' : 'blocked', option: null }
- : resolveCustomerText(options, customerText)
- const customer = fixedCustomer || resolved.option?.customer || null
- const customerBlocked = fixedCustomer ? canInvoice(fixedCustomer) : resolved.option ? { ok: resolved.option.ok, reason: resolved.option.reason } : { ok: true }
- const total = invoiceTotalCents(items)
- // Anschrift des Empfängers (§ 14 Abs. 4 Nr. 1 UStG): ohne sie ist „Senden“ nur bis 250 € möglich
- const gate = invoiceSendGate(customerBlocked.ok ? customer : null, total)
-
- const updateItem = (uid, field, value) => {
- setItems((list) => list.map((it) => (it.uid === uid ? { ...it, [field]: value } : it)))
- if (error) setError('')
- }
-
- async function submit(senden) {
- if (lockRef.current) return
- const check = validateInvoiceForm({ customer, items, days, memo })
- if (!check.ok) {
- setError(check.error)
- return
- }
- if (senden) {
- if (!invoiceSendGate(customer, check.totalCents).sendAllowed) {
- setError(ADDRESS_INCOMPLETE_TEXT)
- return
- }
- // Senden schreibt die Rechnung fest und schickt sie an den Kunden: erst nach Rückfrage
- const sure = window.confirm(
- invoiceSendConfirmText({ name: customerName(customer), email: customer.email, totalCents: check.totalCents })
- )
- if (!sure) return
- }
- lockRef.current = true
- setBusy(senden ? 'send' : 'draft')
- setError('')
- try {
- const data = await createInvoice({ ...check.payload, senden })
- if (onCreated) onCreated(data?.rechnung || null, { senden })
- onClose()
- } catch (err) {
- if (isFinalizedNotSent(err)) {
- // Die Rechnung steht bei Stripe: keine zweite anlegen, die Liste zeigt sie
- if (onCreated) onCreated(null, { senden: true, notice: invoiceFailureMessage(err), tone: 'warn', finalizedFilter: finalizedFilter(err) })
- onClose()
- return
- }
- if (aliveRef.current) setError(invoiceFailureMessage(err))
- } finally {
- lockRef.current = false
- if (aliveRef.current) setBusy(null)
- }
- }
-
- const disabled = busy !== null
-
- const footer = (
-
-
-
-
-
- )
-
- // Enter sendet nie (das wäre eine Mail an den Kunden): das Formular hat keinen Absenden-Knopf, jeder Knopf darin ist type="button"
- return (
-
- )
-}