feat(accounting): incoming-invoices inbox with camera capture + triage/re-bill

Adds the Accounting → Incoming invoices frontend on top of the existing
/api/admin/expenses backend:

- accounting.service.ts: typed client (inbound upload/list/get/update/
  categorize, expense list, re-bill, supplier-payment, categories).
- AccountingInboxPage: capture a supplier invoice via the device CAMERA
  (<input accept="image/*" capture="environment">) or a PDF/image upload;
  inbox list with status badges + parsed summary; a triage modal to confirm
  fields and pick a disposition (re-bill / pass-through / company expense /
  duplicate / declined). Re-bill uses the customer picker and mints an
  editable scheduled invoice (chains categorize -> rebill).
- AccountingLayout: "Incoming invoices" sub-nav item + AccountingIndex that
  redirects /admin/accounting to the first enabled sub-feature.
- App.tsx: /admin/accounting/inbox route (gated by incomingInvoices).
- i18n: accounting.inbox/disposition/markup + subnav.incomingInvoices +
  common.saving (EN + DE, DE authored natively).

Camera capture needs no native app — the mobile web input drives the device
camera straight into the upload endpoint. OCR/QR auto-extraction is still a
backend follow-up (extractionService is a no-op), so fields are confirmed
manually in the triage modal for now.

Verified: npm run build green; en/de JSON valid.
This commit is contained in:
Luca
2026-06-11 00:31:05 +02:00
parent 2c351bf0c9
commit 2b5efebaff
6 changed files with 591 additions and 8 deletions
+6 -2
View File
@@ -64,7 +64,8 @@ import {
import { CustomerAuthProvider } from './contexts/CustomerAuthContext';
import { AdminLayout, AdminAuthWrapper } from './components/admin';
import { ClientsLayout } from './components/admin/ClientsLayout';
import { AccountingLayout } from './components/admin/AccountingLayout';
import { AccountingLayout, AccountingIndex } from './components/admin/AccountingLayout';
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
import { RequireFeature } from './components/admin/RequireFeature';
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
@@ -268,10 +269,13 @@ function App() {
pages. Each sub-route is independently flagged. */}
<Route element={<RequireFeature flag="accounting" />}>
<Route path="accounting" element={<AccountingLayout />}>
<Route element={<RequireFeature flag="incomingInvoices" />}>
<Route path="inbox" element={<AccountingInboxPage />} />
</Route>
<Route element={<RequireFeature flag="taxReport" />}>
<Route path="tax-report" element={<TaxReportPage />} />
</Route>
<Route index element={<Navigate to="/admin/accounting/tax-report" replace />} />
<Route index element={<AccountingIndex />} />
</Route>
</Route>