feat(accounting): clearer tax-export window + gate journal export on accounting flag

- Restructure the export area into two labelled groups: 'Report' (PDF/CSV,
  for you) and 'Accounting journal' (for your accountant), each with a
  one-line caption — instead of two unlabelled button rows.
- i18n: the English label was the German 'Treuhänder export' → now 'Accountant
  export' (de stays 'Treuhänder-Export'); hint reworded.
- Feature flags: the journal export is an accounting-layer feature (needs the
  Chart-of-accounts mapping), so gate it on the 'accounting' master — the
  group only renders when accounting is on, and the backend /export route no
  longer requires the 'taxReport' sub-flag (the router already requires accounting).

Build + node --check + JSON parse green.
This commit is contained in:
Luca
2026-06-15 19:18:36 +02:00
parent b1f73c1df9
commit 3edd832103
4 changed files with 96 additions and 58 deletions
+6 -1
View File
@@ -3938,8 +3938,13 @@
},
"exportPdf": "PDF exportieren",
"ledgerExport": "Treuhänder-Export",
"ledgerExportHint": "Doppelte Buchung für Ihren Treuhänder, abgebildet über Ihren Kontenplan.",
"ledgerExportHint": "Doppelte Buchungssätze für Ihren Treuhänder, abgebildet über Ihren Kontenplan.",
"ledgerExportConfigure": "Einrichten →",
"export": {
"reportTitle": "Bericht",
"reportHint": "Lesbare Liste — für Ihre Unterlagen.",
"journalTitle": "Buchungsjournal"
},
"exportCsv": "CSV exportieren",
"exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.",
"errorTitle": "Steuerliste konnte nicht geladen werden",
+7 -2
View File
@@ -3937,9 +3937,14 @@
"currency": "Currency"
},
"exportPdf": "Export PDF",
"ledgerExport": "Treuhänder export",
"ledgerExportHint": "Double-entry journal for your accountant, mapped via your Chart of accounts.",
"ledgerExport": "Accountant export",
"ledgerExportHint": "Double-entry postings for your accountant, mapped via your Chart of accounts.",
"ledgerExportConfigure": "Configure →",
"export": {
"reportTitle": "Report",
"reportHint": "Readable list — for your own records.",
"journalTitle": "Accounting journal"
},
"exportCsv": "Export CSV",
"exportFailed": "Export failed. Please try again.",
"errorTitle": "Could not load tax report",
@@ -28,6 +28,7 @@ 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 { useFeatureFlags } from '../../../contexts/FeatureFlagsContext';
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
import { toast } from 'react-toastify';
@@ -93,6 +94,7 @@ function triggerBrowserDownload(url: string, filename: string) {
export const TaxReportPage: React.FC = () => {
const { t, i18n } = useTranslation();
const { flags } = useFeatureFlags();
const { format: fmtDate } = useLocalizedDate();
const [preset, setPreset] = useState<PeriodPreset>('thisYear');
const initialPeriod = useMemo(() => periodForPreset('thisYear'), []);
@@ -294,58 +296,82 @@ export const TaxReportPage: React.FC = () => {
</select>
</div>
<div className="space-y-2 pt-1">
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
variant="outline"
onClick={() => handleExport('csv')}
disabled={exportsDisabled}
isLoading={isExporting === 'csv'}
leftIcon={<FileDown className="w-4 h-4" />}
>
{t('taxReport.exportCsv', 'Export CSV')}
</Button>
<Button
variant="primary"
onClick={() => handleExport('pdf')}
disabled={exportsDisabled}
isLoading={isExporting === 'pdf'}
leftIcon={<Download className="w-4 h-4" />}
>
{t('taxReport.exportPdf', 'Export PDF')}
</Button>
{/* Export area — two clearly-separated groups so it's obvious
what each file is and who it's for: the human-readable Report
(PDF/CSV) and the accounting Journal (for the Treuhänder). The
Journal group only shows when the accounting layer is on, since
it needs the Chart-of-accounts mapping. */}
<div className="pt-3 mt-1 border-t border-neutral-200 dark:border-neutral-700 space-y-3">
{/* Group 1 — Report (for you) */}
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{t('taxReport.export.reportTitle', 'Report')}
</div>
<div className="text-[11px] text-neutral-400 dark:text-neutral-500">
{t('taxReport.export.reportHint', 'Readable list — for your own records.')}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
onClick={() => handleExport('csv')}
disabled={exportsDisabled}
isLoading={isExporting === 'csv'}
leftIcon={<FileDown className="w-4 h-4" />}
>
{t('taxReport.exportCsv', 'Export CSV')}
</Button>
<Button
variant="primary"
onClick={() => handleExport('pdf')}
disabled={exportsDisabled}
isLoading={isExporting === 'pdf'}
leftIcon={<Download className="w-4 h-4" />}
>
{t('taxReport.exportPdf', 'Export PDF')}
</Button>
</div>
</div>
{/* Treuhänder collective-journal export — moved here from its own
tab; reuses the same period/currency. Format = target tool. */}
<div className="flex flex-wrap items-center justify-end gap-2">
<select
value={ledgerFormat}
onChange={(e) => setLedgerFormat(e.target.value as ExportFormat)}
disabled={exportsDisabled}
aria-label={t('ledger.export.format', 'Target tool') as string}
className={`${selectClassName} w-auto`}
>
{LEDGER_FORMATS.map((f) => (
<option key={f} value={f}>{t(`ledger.export.format_${f}`, f)}</option>
))}
</select>
<Button
variant="outline"
onClick={handleLedgerExport}
disabled={exportsDisabled}
isLoading={isExporting === 'ledger'}
leftIcon={<FileSpreadsheet className="w-4 h-4" />}
>
{t('taxReport.ledgerExport', 'Treuhänder export')}
</Button>
</div>
<p className="text-[11px] text-neutral-400 dark:text-neutral-500 text-right">
{t('taxReport.ledgerExportHint', 'Double-entry journal for your accountant, mapped via your Chart of accounts.')}{' '}
<Link to="/admin/accounting/ledger" className="underline hover:text-neutral-600 dark:hover:text-neutral-300">
{t('taxReport.ledgerExportConfigure', 'Configure →')}
</Link>
</p>
{/* Group 2 — Accounting journal (for your accountant) */}
{flags.accounting && (
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 pt-3 border-t border-dashed border-neutral-200 dark:border-neutral-800">
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{t('taxReport.export.journalTitle', 'Accounting journal')}
</div>
<div className="text-[11px] text-neutral-400 dark:text-neutral-500">
{t('taxReport.ledgerExportHint', 'Double-entry postings for your accountant, mapped via your Chart of accounts.')}{' '}
<Link to="/admin/accounting/ledger" className="underline hover:text-neutral-600 dark:hover:text-neutral-300">
{t('taxReport.ledgerExportConfigure', 'Configure →')}
</Link>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<select
value={ledgerFormat}
onChange={(e) => setLedgerFormat(e.target.value as ExportFormat)}
disabled={exportsDisabled}
aria-label={t('ledger.export.format', 'Target tool') as string}
className={`${selectClassName} w-auto`}
>
{LEDGER_FORMATS.map((f) => (
<option key={f} value={f}>{t(`ledger.export.format_${f}`, f)}</option>
))}
</select>
<Button
variant="outline"
onClick={handleLedgerExport}
disabled={exportsDisabled}
isLoading={isExporting === 'ledger'}
leftIcon={<FileSpreadsheet className="w-4 h-4" />}
>
{t('taxReport.ledgerExport', 'Accountant export')}
</Button>
</div>
</div>
)}
</div>
</div>
</Card>