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.')}
-
- {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 →')}
+
+