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:
@@ -6,9 +6,9 @@
|
|||||||
* /mappings GET/PATCH category→account + default-account/VAT settings
|
* /mappings GET/PATCH category→account + default-account/VAT settings
|
||||||
* /export GET Treuhänder collective-journal CSV (generic|banana|bexio)
|
* /export GET Treuhänder collective-journal CSV (generic|banana|bexio)
|
||||||
*
|
*
|
||||||
* Gated by the `accounting` master flag; export additionally requires the
|
* Gated by the `accounting` master flag (all routes, incl. /export). Uses the
|
||||||
* `taxReport` sub-flag (it's the export umbrella). Uses the `accounting.*`
|
* `accounting.*` permissions. Output is a GUIDELINE — the UI carries the
|
||||||
* permissions. Output is a GUIDELINE — the UI carries the Treuhänder caveat.
|
* Treuhänder caveat.
|
||||||
*/
|
*/
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { body, param, query } = require('express-validator');
|
const { body, param, query } = require('express-validator');
|
||||||
@@ -32,7 +32,6 @@ function requireFlag(key, code) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const requireAccounting = requireFlag('accounting', 'ACCOUNTING_DISABLED');
|
const requireAccounting = requireFlag('accounting', 'ACCOUNTING_DISABLED');
|
||||||
const requireTaxReport = requireFlag('taxReport', 'TAX_REPORT_DISABLED');
|
|
||||||
|
|
||||||
router.use(adminAuth);
|
router.use(adminAuth);
|
||||||
router.use(requireAccounting);
|
router.use(requireAccounting);
|
||||||
@@ -107,7 +106,10 @@ router.patch('/mappings/settings', requirePermission('accounting.manage'), handl
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// ── Treuhänder export ────────────────────────────────────────────────
|
// ── Treuhänder export ────────────────────────────────────────────────
|
||||||
router.get('/export', requireTaxReport, requirePermission('bills.view'),
|
// Gated by the router-level `accounting` flag only — the export lives on the
|
||||||
|
// Tax page now but is an accounting-layer feature (needs the chart-of-accounts
|
||||||
|
// mapping), so it no longer requires the `taxReport` sub-flag.
|
||||||
|
router.get('/export', requirePermission('bills.view'),
|
||||||
[query('from').matches(/^\d{4}-\d{2}-\d{2}$/), query('to').matches(/^\d{4}-\d{2}-\d{2}$/),
|
[query('from').matches(/^\d{4}-\d{2}-\d{2}$/), query('to').matches(/^\d{4}-\d{2}-\d{2}$/),
|
||||||
query('currency').matches(/^[A-Za-z]{3}$/), query('format').optional().isIn(ledgerService.EXPORT_FORMATS)],
|
query('currency').matches(/^[A-Za-z]{3}$/), query('format').optional().isIn(ledgerService.EXPORT_FORMATS)],
|
||||||
handleAsync(async (req, res) => {
|
handleAsync(async (req, res) => {
|
||||||
|
|||||||
@@ -3938,8 +3938,13 @@
|
|||||||
},
|
},
|
||||||
"exportPdf": "PDF exportieren",
|
"exportPdf": "PDF exportieren",
|
||||||
"ledgerExport": "Treuhänder-Export",
|
"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 →",
|
"ledgerExportConfigure": "Einrichten →",
|
||||||
|
"export": {
|
||||||
|
"reportTitle": "Bericht",
|
||||||
|
"reportHint": "Lesbare Liste — für Ihre Unterlagen.",
|
||||||
|
"journalTitle": "Buchungsjournal"
|
||||||
|
},
|
||||||
"exportCsv": "CSV exportieren",
|
"exportCsv": "CSV exportieren",
|
||||||
"exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.",
|
"exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.",
|
||||||
"errorTitle": "Steuerliste konnte nicht geladen werden",
|
"errorTitle": "Steuerliste konnte nicht geladen werden",
|
||||||
|
|||||||
@@ -3937,9 +3937,14 @@
|
|||||||
"currency": "Currency"
|
"currency": "Currency"
|
||||||
},
|
},
|
||||||
"exportPdf": "Export PDF",
|
"exportPdf": "Export PDF",
|
||||||
"ledgerExport": "Treuhänder export",
|
"ledgerExport": "Accountant export",
|
||||||
"ledgerExportHint": "Double-entry journal for your accountant, mapped via your Chart of accounts.",
|
"ledgerExportHint": "Double-entry postings for your accountant, mapped via your Chart of accounts.",
|
||||||
"ledgerExportConfigure": "Configure →",
|
"ledgerExportConfigure": "Configure →",
|
||||||
|
"export": {
|
||||||
|
"reportTitle": "Report",
|
||||||
|
"reportHint": "Readable list — for your own records.",
|
||||||
|
"journalTitle": "Accounting journal"
|
||||||
|
},
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
"exportFailed": "Export failed. Please try again.",
|
"exportFailed": "Export failed. Please try again.",
|
||||||
"errorTitle": "Could not load tax report",
|
"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';
|
'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 { taxReportService, type TaxReportParams } from '../../../services/taxReport.service';
|
||||||
import { ledgerService, type ExportFormat } from '../../../services/ledger.service';
|
import { ledgerService, type ExportFormat } from '../../../services/ledger.service';
|
||||||
|
import { useFeatureFlags } from '../../../contexts/FeatureFlagsContext';
|
||||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ function triggerBrowserDownload(url: string, filename: string) {
|
|||||||
|
|
||||||
export const TaxReportPage: React.FC = () => {
|
export const TaxReportPage: React.FC = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const { flags } = useFeatureFlags();
|
||||||
const { format: fmtDate } = useLocalizedDate();
|
const { format: fmtDate } = useLocalizedDate();
|
||||||
const [preset, setPreset] = useState<PeriodPreset>('thisYear');
|
const [preset, setPreset] = useState<PeriodPreset>('thisYear');
|
||||||
const initialPeriod = useMemo(() => periodForPreset('thisYear'), []);
|
const initialPeriod = useMemo(() => periodForPreset('thisYear'), []);
|
||||||
@@ -294,8 +296,23 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 pt-1">
|
{/* Export area — two clearly-separated groups so it's obvious
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleExport('csv')}
|
onClick={() => handleExport('csv')}
|
||||||
@@ -315,10 +332,23 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
{t('taxReport.exportPdf', 'Export PDF')}
|
{t('taxReport.exportPdf', 'Export PDF')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Treuhänder collective-journal export — moved here from its own
|
{/* Group 2 — Accounting journal (for your accountant) */}
|
||||||
tab; reuses the same period/currency. Format = target tool. */}
|
{flags.accounting && (
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
<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
|
<select
|
||||||
value={ledgerFormat}
|
value={ledgerFormat}
|
||||||
onChange={(e) => setLedgerFormat(e.target.value as ExportFormat)}
|
onChange={(e) => setLedgerFormat(e.target.value as ExportFormat)}
|
||||||
@@ -337,15 +367,11 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
isLoading={isExporting === 'ledger'}
|
isLoading={isExporting === 'ledger'}
|
||||||
leftIcon={<FileSpreadsheet className="w-4 h-4" />}
|
leftIcon={<FileSpreadsheet className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
{t('taxReport.ledgerExport', 'Treuhänder export')}
|
{t('taxReport.ledgerExport', 'Accountant export')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-neutral-400 dark:text-neutral-500 text-right">
|
</div>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user