diff --git a/frontend/src/components/admin/VatCodesManager.tsx b/frontend/src/components/admin/VatCodesManager.tsx index f5972760..2c544dc2 100644 --- a/frontend/src/components/admin/VatCodesManager.tsx +++ b/frontend/src/components/admin/VatCodesManager.tsx @@ -20,7 +20,16 @@ import { const labelCls = 'block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1'; const selectCls = 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm'; const TAX_TREATMENTS = ['domestic', 'reverse_charge_service', 'foreign_vat_non_reclaimable', 'import_goods']; -const OUTPUT_RATES = ['8.1', '2.6', '3.8', '0']; + +// Mirror backend ledgerService.rateKey so the map keys we write match the +// lookup at export time (`outputVatMap[rateKey(inv.vat_rate)]`). 8.10 → '8.1', +// 0 → '0', 19 → '19'. Keeping these in sync is what lets a user retype their +// codes to local rates and have the revenue-rate rows follow automatically. +const rateKey = (rate: number | string): string => { + const n = Number(rate); + if (!Number.isFinite(n)) return '0'; + return String(Number(n.toFixed(2))); +}; const VatModal: React.FC<{ vat?: VatCode; accounts: LedgerAccount[]; onClose: () => void; onDone: () => void }> = ({ vat, accounts, onClose, onDone }) => { const { t } = useTranslation(); @@ -96,6 +105,21 @@ export const VatCodesManager: React.FC = () => { const inputVat = useMemo(() => (vatCodes ?? []).filter((v) => v.direction === 'input'), [vatCodes]); const outputVat = useMemo(() => (vatCodes ?? []).filter((v) => v.direction === 'output'), [vatCodes]); + // Revenue-rate rows are DATA-DRIVEN: the distinct rates of the output codes + // (first-seen order), not a hardcoded Swiss list. Retype a code to a local + // rate (e.g. 19) and its row appears here automatically; remove the last code + // at a rate and that row drops. Seeds (8.1/2.6/3.8/0) just produce the same + // four rows they did before. + const outputRates = useMemo(() => { + const seen = new Set(); + const rates: string[] = []; + for (const v of outputVat) { + const k = rateKey(v.rate); + if (!seen.has(k)) { seen.add(k); rates.push(k); } + } + return rates; + }, [outputVat]); + const refetch = () => { qc.invalidateQueries({ queryKey: ['ledger-vat-codes'] }); qc.invalidateQueries({ queryKey: ['ledger-mappings'] }); }; const delVat = useMutation({ @@ -160,17 +184,21 @@ export const VatCodesManager: React.FC = () => { {/* Rate→code + treatment→code maps */}

{t('ledger.outputVatMap.title', 'VAT code by revenue rate')}

-
- {OUTPUT_RATES.map((rate) => ( -
- - -
- ))} -
+ {outputRates.length === 0 ? ( +

{t('ledger.outputVatMap.empty', 'Add output VAT codes above to configure a code per revenue rate.')}

+ ) : ( +
+ {outputRates.map((rate) => ( +
+ + +
+ ))} +
+ )}

{t('ledger.vatMap.title', 'VAT code by tax treatment (costs)')}

diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 32e1e37a..49b65a75 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -3903,7 +3903,7 @@ "ledger_account_rebilled_revenue": "Weiterverrechnete Spesen (Ertrag)" }, "vatMap": { "title": "MWST-Code nach steuerlicher Behandlung (Kosten)" }, - "outputVatMap": { "title": "MWST-Code nach Umsatzsatz" }, + "outputVatMap": { "title": "MWST-Code nach Umsatzsatz", "empty": "Fügen Sie oben Ausgangs-MWST-Codes hinzu, um pro Umsatzsatz einen Code festzulegen." }, "categoryMap": { "title": "Aufwandkategorie → Konto" }, "accounts": { "title": "Kontenplan" }, "vatCodes": { "title": "MWST-Codes" }, diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index a0b5c623..5a8cfdee 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -3903,7 +3903,7 @@ "ledger_account_rebilled_revenue": "Re-billed expenses (revenue)" }, "vatMap": { "title": "VAT code by tax treatment (costs)" }, - "outputVatMap": { "title": "VAT code by revenue rate" }, + "outputVatMap": { "title": "VAT code by revenue rate", "empty": "Add output VAT codes above to configure a code per revenue rate." }, "categoryMap": { "title": "Expense category → account" }, "accounts": { "title": "Chart of accounts" }, "vatCodes": { "title": "VAT codes" },