From b1f73c1df9408ddae821760eb8ed57c726d2e056 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:57:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(accounting):=20move=20Treuh=C3=A4nder=20ex?= =?UTF-8?q?port=20onto=20the=20Tax=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone 'Treuhänder export' tab duplicated the Tax page's period/ currency filters over the same data. Fold the collective-journal export into the Tax page as a third export action (target-tool format picker: generic / Banana / bexio), beside Export CSV/PDF, with a link to its Chart-of-accounts config. Removes the Accounting sub-nav 'export' tab (old /export route now redirects to the Tax page); keeps Chart of accounts as its own setup tab. Deletes the now-orphaned LedgerExportPage. Build + JSON parse green. --- frontend/src/App.tsx | 5 +- .../src/components/admin/AccountingLayout.tsx | 11 +- frontend/src/i18n/locales/de.json | 3 + frontend/src/i18n/locales/en.json | 3 + .../admin/accounting/LedgerExportPage.tsx | 126 ------------------ .../src/pages/admin/clients/TaxReportPage.tsx | 98 +++++++++++--- 6 files changed, 89 insertions(+), 157 deletions(-) delete mode 100644 frontend/src/pages/admin/accounting/LedgerExportPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 131d14ae..9079fe6f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -70,7 +70,6 @@ import { AccountingLayout, AccountingIndex } from './components/admin/Accounting import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage'; import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage'; import { ChartOfAccountsPage } from './pages/admin/accounting/ChartOfAccountsPage'; -import { LedgerExportPage } from './pages/admin/accounting/LedgerExportPage'; import { RequireFeature } from './components/admin/RequireFeature'; import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common'; import { MaintenanceWrapper } from './components/MaintenanceWrapper'; @@ -288,7 +287,9 @@ function App() { }> } /> - } /> + {/* Treuhänder export moved onto the Tax page; keep + the old path working for bookmarks. */} + } /> {/* Chart of accounts + VAT codes (Layer A) — gated by the accounting master flag alongside the section. */} diff --git a/frontend/src/components/admin/AccountingLayout.tsx b/frontend/src/components/admin/AccountingLayout.tsx index ca354fa5..061a911a 100644 --- a/frontend/src/components/admin/AccountingLayout.tsx +++ b/frontend/src/components/admin/AccountingLayout.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Landmark, Calculator, Inbox, Wallet, BookOpen, FileSpreadsheet } from 'lucide-react'; +import { Landmark, Calculator, Inbox, Wallet, BookOpen } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext'; @@ -45,18 +45,13 @@ export const AccountingLayout: React.FC = () => { }, { key: 'tax-report', + // The Treuhänder export now lives ON the Tax page (same period/currency + // filters, same data) instead of a separate sub-tab — see TaxReportPage. to: '/admin/accounting/tax-report', label: t('accounting.subnav.taxReport', 'Tax'), icon: Calculator, featureFlag: 'taxReport', }, - { - key: 'export', - to: '/admin/accounting/export', - label: t('accounting.subnav.export', 'Treuhänder export'), - icon: FileSpreadsheet, - featureFlag: 'taxReport', - }, { key: 'ledger', to: '/admin/accounting/ledger', diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index bc7a7a9e..1e14f722 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -3937,6 +3937,9 @@ "currency": "Währung" }, "exportPdf": "PDF exportieren", + "ledgerExport": "Treuhänder-Export", + "ledgerExportHint": "Doppelte Buchung für Ihren Treuhänder, abgebildet über Ihren Kontenplan.", + "ledgerExportConfigure": "Einrichten →", "exportCsv": "CSV exportieren", "exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.", "errorTitle": "Steuerliste konnte nicht geladen werden", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 964d0704..a75e69aa 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -3937,6 +3937,9 @@ "currency": "Currency" }, "exportPdf": "Export PDF", + "ledgerExport": "Treuhänder export", + "ledgerExportHint": "Double-entry journal for your accountant, mapped via your Chart of accounts.", + "ledgerExportConfigure": "Configure →", "exportCsv": "Export CSV", "exportFailed": "Export failed. Please try again.", "errorTitle": "Could not load tax report", diff --git a/frontend/src/pages/admin/accounting/LedgerExportPage.tsx b/frontend/src/pages/admin/accounting/LedgerExportPage.tsx deleted file mode 100644 index 737bff3a..00000000 --- a/frontend/src/pages/admin/accounting/LedgerExportPage.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Accounting → Treuhänder export (Layer A). - * - * Picks a period + currency + target tool, then downloads the collective - * journal (accrual Buchungssätze) as CSV for import into the Treuhänder's - * double-entry software. Output is a guideline — disclaimer on the page. - */ -import React, { useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; -import { FileSpreadsheet, Download, AlertCircle } from 'lucide-react'; -import { Button, Card, CardContent, LocalizedDateInput } from '../../../components/common'; -import { ledgerService, type ExportFormat } from '../../../services/ledger.service'; - -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 text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500'; -const labelCls = 'block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1'; - -type PeriodPreset = 'thisYear' | 'lastYear' | 'thisQuarter' | 'lastQuarter' | 'custom'; -const FORMATS: ExportFormat[] = ['generic', 'banana', 'bexio']; - -function periodForPreset(preset: PeriodPreset, today = new Date()): { from: string; to: string } { - const y = today.getFullYear(); - const pad = (n: number) => String(n).padStart(2, '0'); - if (preset === 'thisYear') return { from: `${y}-01-01`, to: `${y}-12-31` }; - if (preset === 'lastYear') return { from: `${y - 1}-01-01`, to: `${y - 1}-12-31` }; - const quarter = Math.floor(today.getMonth() / 3); - if (preset === 'thisQuarter') { - const sm = quarter * 3; const em = sm + 2; - return { from: `${y}-${pad(sm + 1)}-01`, to: `${y}-${pad(em + 1)}-${pad(new Date(y, em + 1, 0).getDate())}` }; - } - let qy = y; let q = quarter - 1; if (q < 0) { q = 3; qy = y - 1; } - const sm = q * 3; const em = sm + 2; - return { from: `${qy}-${pad(sm + 1)}-01`, to: `${qy}-${pad(em + 1)}-${pad(new Date(qy, em + 1, 0).getDate())}` }; -} - -function triggerDownload(url: string, filename: string) { - const a = document.createElement('a'); - a.href = url; a.download = filename; a.style.display = 'none'; - document.body.appendChild(a); a.click(); document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 4000); -} - -export const LedgerExportPage: React.FC = () => { - const { t } = useTranslation(); - const [preset, setPreset] = useState('thisYear'); - const initial = useMemo(() => periodForPreset('thisYear'), []); - const [from, setFrom] = useState(initial.from); - const [to, setTo] = useState(initial.to); - const [currency, setCurrency] = useState('CHF'); - const [format, setFormat] = useState('generic'); - const [busy, setBusy] = useState(false); - - const onPreset = (next: PeriodPreset) => { - setPreset(next); - if (next !== 'custom') { const p = periodForPreset(next); setFrom(p.from); setTo(p.to); } - }; - - const handleExport = async () => { - setBusy(true); - try { - const { url, filename } = await ledgerService.downloadExportUrl({ from, to, currency, format }); - triggerDownload(url, filename); - } catch (e: any) { - toast.error(e?.response?.data?.error || e.message || t('ledger.export.failed', 'Export failed.')); - } finally { setBusy(false); } - }; - - return ( - - -
-
- -
-
-

{t('ledger.export.title', 'Treuhänder export')}

-

{t('ledger.export.intro', 'Download the collective journal (revenue + costs as accrual postings with account and VAT codes) for import into your Treuhänder’s accounting software.')}

-
-
- -
-
- - -
-
-
{ setFrom(iso); setPreset('custom'); }} />
-
{ setTo(iso); setPreset('custom'); }} />
-
-
-
- - -
-
- - -
-
-
- -
-
- -

- - {t('ledger.export.disclaimer', 'Accrual basis only (document dates) — payments/bank movements are not included. Account + VAT codes follow your Chart-of-accounts mapping. Always review the import with your Treuhänder before filing.')} -

-
-
- ); -}; - -export default LedgerExportPage; diff --git a/frontend/src/pages/admin/clients/TaxReportPage.tsx b/frontend/src/pages/admin/clients/TaxReportPage.tsx index c9f2d4af..3cc80480 100644 --- a/frontend/src/pages/admin/clients/TaxReportPage.tsx +++ b/frontend/src/pages/admin/clients/TaxReportPage.tsx @@ -17,7 +17,8 @@ import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { Calculator, Download, FileDown, AlertCircle } from 'lucide-react'; +import { Calculator, Download, FileDown, FileSpreadsheet, AlertCircle } from 'lucide-react'; +import { Link } from 'react-router-dom'; import { Button, Card, Loading, LocalizedDateInput } from '../../../components/common'; // Lightweight native select styled to match Input — the common barrel @@ -26,9 +27,12 @@ import { Button, Card, Loading, LocalizedDateInput } from '../../../components/c const selectClassName = 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500'; import { taxReportService, type TaxReportParams } from '../../../services/taxReport.service'; +import { ledgerService, type ExportFormat } from '../../../services/ledger.service'; import { useLocalizedDate } from '../../../hooks/useLocalizedDate'; import { toast } from 'react-toastify'; +const LEDGER_FORMATS: ExportFormat[] = ['generic', 'banana', 'bexio']; + type PeriodPreset = 'thisYear' | 'lastYear' | 'thisQuarter' | 'lastQuarter' | 'custom'; function isoDate(d: Date): string { @@ -95,7 +99,10 @@ export const TaxReportPage: React.FC = () => { const [from, setFrom] = useState(initialPeriod.from); const [to, setTo] = useState(initialPeriod.to); const [currency, setCurrency] = useState('CHF'); - const [isExporting, setIsExporting] = useState<'pdf' | 'csv' | null>(null); + const [isExporting, setIsExporting] = useState<'pdf' | 'csv' | 'ledger' | null>(null); + // Treuhänder (collective-journal) export — same period/currency as the + // report; target tool picks the import format (generic / Banana / bexio). + const [ledgerFormat, setLedgerFormat] = useState('generic'); // Unified-ledger sort (#5). Defaults to date ascending — matches the // server-side order so the first paint is stable. const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' }>({ key: 'date', dir: 'asc' }); @@ -136,6 +143,22 @@ export const TaxReportPage: React.FC = () => { } }; + // Treuhänder collective-journal export (double-entry postings with account + + // VAT codes) — reuses the page's period/currency, adds the target-tool format. + const handleLedgerExport = async () => { + setIsExporting('ledger'); + try { + const { url, filename } = await ledgerService.downloadExportUrl({ from, to, currency, format: ledgerFormat }); + triggerBrowserDownload(url, filename); + } catch (err: any) { + toast.error(err?.response?.data?.error || t('taxReport.exportFailed', 'Export failed. Please try again.')); + // eslint-disable-next-line no-console + console.error(err); + } finally { + setIsExporting(null); + } + }; + // Per maintainer: every CH/LI/DE/AT-based business writes 1'000.00 // regardless of document language, so we default to de-CH (the only // Intl locale producing apostrophe thousands). Non-DACH operators @@ -271,25 +294,58 @@ export const TaxReportPage: React.FC = () => { -
- - +
+
+ + +
+ + {/* Treuhänder collective-journal export — moved here from its own + tab; reuses the same period/currency. Format = target tool. */} +
+ + +
+

+ {t('taxReport.ledgerExportHint', 'Double-entry journal for your accountant, mapped via your Chart of accounts.')}{' '} + + {t('taxReport.ledgerExportConfigure', 'Configure →')} + +