diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 3baf793a..426c3c8a 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -3502,6 +3502,7 @@ "categorizedToast": "Dokument kategorisiert.", "triageTitle": "Dokument kategorisieren", "saveCategorize": "Speichern", + "saveCategorizePay": "Speichern & als bezahlt markieren", "categorize": "Kategorisieren", "view": "Ansehen", "empty": "Noch keine Dokumente — oben eines erfassen.", @@ -3532,7 +3533,9 @@ "declineReason": "Grund", "customer": "Kunde", "eventId": "Event-ID (optional)", - "markup": "Zuschlag" + "markup": "Zuschlag", + "reference": "Zahlungsreferenz", + "referenceHint": "QR-/ESR-Referenz oder Mitteilung" } }, "expenseStatus": { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6ea28679..5045fabc 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -3502,6 +3502,7 @@ "categorizedToast": "Document categorized.", "triageTitle": "Categorize document", "saveCategorize": "Save", + "saveCategorizePay": "Save & mark paid", "categorize": "Categorize", "view": "View", "empty": "No documents yet — capture one above.", @@ -3532,7 +3533,9 @@ "declineReason": "Reason", "customer": "Client", "eventId": "Event ID (optional)", - "markup": "Markup" + "markup": "Markup", + "reference": "Payment reference", + "referenceHint": "QR / ESR reference or message" } }, "expenseStatus": { diff --git a/frontend/src/pages/admin/accounting/AccountingInboxPage.tsx b/frontend/src/pages/admin/accounting/AccountingInboxPage.tsx index ffcc0541..9938e8f2 100644 --- a/frontend/src/pages/admin/accounting/AccountingInboxPage.tsx +++ b/frontend/src/pages/admin/accounting/AccountingInboxPage.tsx @@ -162,12 +162,13 @@ const ViewModal: React.FC<{ doc: InboundDocument; onClose: () => void }> = ({ do ); }; -const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[]; onClose: () => void; onDone: () => void }> = ({ doc, categories, onClose, onDone }) => { +const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[]; onClose: () => void; onDone: (pay?: boolean) => void }> = ({ doc, categories, onClose, onDone }) => { const { t } = useTranslation(); const [supplier, setSupplier] = useState(doc.supplierName || ''); const [amountMajor, setAmountMajor] = useState(doc.totalAmountMinor != null ? doc.totalAmountMinor / 100 : NaN); const [currency, setCurrency] = useState(doc.currency || 'CHF'); const [invoiceDate, setInvoiceDate] = useState(doc.invoiceDate || ''); + const [reference, setReference] = useState(doc.paymentReference || ''); const [disposition, setDisposition] = useState('eigener_aufwand'); const [categoryId, setCategoryId] = useState(undefined); const [eventId, setEventId] = useState(null); @@ -183,9 +184,11 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[ markupFlatMinor: markupType === 'flat' && Number.isFinite(markupValue) ? Math.round(markupValue * 100) : null, }); + // `pay` is threaded through as the mutation variable so onSuccess can decide + // whether to continue into the mark-paid step (#5). const save = useMutation({ - mutationFn: async () => { - await accountingService.updateInbound(doc.id, { supplierName: supplier || null, totalAmountMinor: totalMinor, currency: currency || null, invoiceDate: invoiceDate || null }); + mutationFn: async (_pay: boolean) => { + await accountingService.updateInbound(doc.id, { supplierName: supplier || null, totalAmountMinor: totalMinor, currency: currency || null, invoiceDate: invoiceDate || null, paymentReference: reference || null }); await accountingService.categorizeInbound(doc.id, { disposition, eventId: BOOKING_DISPOSITIONS.includes(disposition) ? eventId : null, @@ -194,7 +197,7 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[ ...markupPayload(), }); }, - onSuccess: () => { toast.success(t('accounting.incoming.categorizedToast', 'Categorized.')); onDone(); }, + onSuccess: (_data, pay) => { toast.success(t('accounting.incoming.categorizedToast', 'Categorized.')); onDone(pay); }, onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'), }); @@ -216,6 +219,7 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[
setCurrency(e.target.value.toUpperCase())} />
+
setReference(e.target.value)} placeholder={t('accounting.inbox.field.referenceHint', 'QR / ESR reference or message') as string} />
@@ -256,9 +260,11 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[ )}
-
+
- + {/* #5: categorize-only OR categorize then continue to mark paid. */} + +
@@ -295,6 +301,18 @@ export const AccountingInboxPage: React.FC = () => { const file = e.target.files?.[0]; if (file) upload.mutate({ file, source }); e.target.value = ''; }; const refresh = () => qc.invalidateQueries({ queryKey: ['accounting-inbound'] }); + + // #5: after categorizing, optionally continue straight into the mark-paid + // dialog (re-fetch so the reference just entered prefills the pay form). + const handleTriageDone = async (pay?: boolean) => { + const id = triageDoc?.id; + setTriageDoc(null); + refresh(); + if (pay && id != null) { + try { setPayDoc(await accountingService.getInbound(id)); } catch (_e) { /* leave it categorized */ } + } + }; + const items = data?.items ?? []; return ( @@ -320,10 +338,14 @@ export const AccountingInboxPage: React.FC = () => {
{items.map((doc) => (
- {/* Click anywhere on the summary to re-view the document — #1. */} -
)} - {triageDoc && setTriageDoc(null)} onDone={() => { setTriageDoc(null); refresh(); }} />} + {triageDoc && setTriageDoc(null)} onDone={handleTriageDone} />} {payDoc && setPayDoc(null)} onDone={() => { setPayDoc(null); refresh(); }} />} {viewDoc && setViewDoc(null)} />}