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
@@ -7,9 +7,9 @@
* expenses pages slot in as additional sub-nav entries when their UIs land.
*/
import React from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Landmark, Calculator } from 'lucide-react';
import { Landmark, Calculator, Inbox } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
@@ -29,6 +29,13 @@ export const AccountingLayout: React.FC = () => {
const { flags } = useFeatureFlags();
const navItems: NavItem[] = [
{
key: 'inbox',
to: '/admin/accounting/inbox',
label: t('accounting.subnav.incomingInvoices', 'Incoming invoices'),
icon: Inbox,
featureFlag: 'incomingInvoices',
},
{
key: 'tax-report',
to: '/admin/accounting/tax-report',
@@ -36,10 +43,7 @@ export const AccountingLayout: React.FC = () => {
icon: Calculator,
featureFlag: 'taxReport',
},
// Future Accounting sub-features (inbound inbox, expenses) slot in here
// once their pages land, e.g.:
// { key: 'inbox', to: '/admin/accounting/inbox', featureFlag: 'accounting' }
// { key: 'expenses', to: '/admin/accounting/expenses', featureFlag: 'accounting' }
// Future: expenses ledger, Erfolgsrechnung.
];
const enabledItems = navItems.filter((item) => flags[item.featureFlag]);
@@ -139,3 +143,15 @@ export const AccountingLayout: React.FC = () => {
</div>
);
};
/**
* Index redirect for /admin/accounting — send to the first enabled
* sub-feature (Incoming invoices preferred, then Tax export). When none
* are on, render nothing; AccountingLayout shows its empty state.
*/
export const AccountingIndex: React.FC = () => {
const { flags } = useFeatureFlags();
if (flags.incomingInvoices) return <Navigate to="/admin/accounting/inbox" replace />;
if (flags.taxReport) return <Navigate to="/admin/accounting/tax-report" replace />;
return null;
};