refactor(accounting): make Accounting a master flag with sub-toggles
Replaces the earlier peer-`accounting` flag (which only *conditionally*
relocated Tax) with a cleaner top-level master + sub-toggle model, per design
discussion:
- `accounting` = explicit top-level MASTER (Settings -> Features). Off hides
the whole Accounting section.
- Sub-toggles, gated under the master:
- `taxReport` ("Tax export") moves PERMANENTLY out of CRM. Removed from the
Clients sub-nav and from the derived `clients` flag. Now INDEPENDENT of
Bills (per decision). Old /admin/clients/tax-report -> redirect to
/admin/accounting/tax-report.
- `incomingInvoices` (new) gates the supplier-invoice capture / expenses /
re-bill feature; the /api/admin/expenses router now checks it.
- Dependency rules (backend + frontend): accounting off forces taxReport +
incomingInvoices off; taxReport dropped from the clients derivation; the
bills->taxReport rule removed.
- Preserve visuals: migration 122 rewritten to auto-enable `accounting` on
installs that already had Tax on (so the tab doesn't vanish), and to seed
`incomingInvoices` off. Verified with a SQLite harness (taxReport on ->
accounting on; off -> off).
- Settings -> Features: new "Accounting" section with the master card + Tax
export + Incoming invoices sub-cards (disabled until the master is on).
- i18n: navigation.accounting, accounting.*, settings.features.{accounting,
incomingInvoices,taxReport.requiresAccounting}, sections.accounting (EN + DE,
DE authored natively); Tax report relabelled "Tax export"/"Steuerexport".
Verified: node -c, migration-122 harness, en/de JSON valid, npm run build green.
This commit is contained in:
@@ -244,10 +244,10 @@ function App() {
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Tax / Steuer report — gated by `taxReport`. */}
|
||||
<Route element={<RequireFeature flag="taxReport" />}>
|
||||
<Route path="tax-report" element={<TaxReportPage />} />
|
||||
</Route>
|
||||
{/* Tax export moved permanently to the Accounting
|
||||
section. Keep this path as a redirect so old
|
||||
bookmarks / links don't 404. */}
|
||||
<Route path="tax-report" element={<Navigate to="/admin/accounting/tax-report" replace />} />
|
||||
{/* Developer tools — gated by `crmDevelopment`. */}
|
||||
<Route element={<RequireFeature flag="crmDevelopment" />}>
|
||||
<Route path="development" element={<CrmDevelopmentPage />} />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import React from 'react';
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Briefcase, UserCog, FileText, Receipt, Wrench, Calculator, Clock, ScrollText, Calendar } from 'lucide-react';
|
||||
import { Briefcase, UserCog, FileText, Receipt, Wrench, Clock, ScrollText, Calendar } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
||||
|
||||
@@ -81,13 +81,8 @@ export const ClientsLayout: React.FC = () => {
|
||||
icon: Receipt,
|
||||
featureFlag: 'bills',
|
||||
},
|
||||
{
|
||||
key: 'tax-report',
|
||||
to: '/admin/clients/tax-report',
|
||||
label: t('clients.subnav.taxReport', 'Tax'),
|
||||
icon: Calculator,
|
||||
featureFlag: 'taxReport',
|
||||
},
|
||||
// Tax export moved permanently to the Accounting section (it is no
|
||||
// longer a CRM sub-feature). See AccountingLayout.
|
||||
// Future sub-features:
|
||||
// { key: 'messaging', ... featureFlag: 'messaging' }
|
||||
{
|
||||
@@ -99,13 +94,7 @@ export const ClientsLayout: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const enabledItems = navItems.filter((item) => {
|
||||
if (!flags[item.featureFlag]) return false;
|
||||
// When the Accounting area is enabled, the tax report relocates out
|
||||
// of CRM and under Accounting — hide it here so it isn't in both.
|
||||
if (item.key === 'tax-report' && flags.accounting) return false;
|
||||
return true;
|
||||
});
|
||||
const enabledItems = navItems.filter((item) => flags[item.featureFlag]);
|
||||
|
||||
// When the parent `clients` flag is on but no sub-feature is enabled,
|
||||
// there's nothing to render. Settings → Features is one click away
|
||||
|
||||
@@ -45,10 +45,12 @@ export const DEFAULT_FLAGS: FeatureFlags = {
|
||||
// Settings → Features once they've reviewed the seeded block
|
||||
// library with their lawyer.
|
||||
contracts: false,
|
||||
// Accounting (migration 122). Top-level Accounting area (inbound
|
||||
// supplier invoices, expenses + re-bill). When ON, the tax report
|
||||
// moves out of the CRM sub-nav and under Accounting. Strictly opt-in.
|
||||
// Accounting (migration 122). Top-level MASTER for the Accounting
|
||||
// section (separate from CRM). Sub-features below require it.
|
||||
accounting: false,
|
||||
// Incoming invoices (migration 124) — supplier-invoice capture +
|
||||
// expenses + re-bill. Accounting sub-feature; requires `accounting`.
|
||||
incomingInvoices: false,
|
||||
};
|
||||
|
||||
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
|
||||
@@ -80,7 +82,12 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|
||||
out.galleries = true; // foundation — always on
|
||||
if (out.quotes === false) out.bills = false; // bills depend on quotes
|
||||
if (out.calendar === false) out.calendarBooking = false; // booking depends on calendar
|
||||
if (out.bills === false) out.taxReport = false; // tax report depends on bills
|
||||
// Accounting sub-features require the Accounting master. Tax export is
|
||||
// independent of Bills now — it relocated permanently into Accounting.
|
||||
if (out.accounting === false) {
|
||||
out.taxReport = false;
|
||||
out.incomingInvoices = false;
|
||||
}
|
||||
// Clients parent flag is DERIVED from its children. Admins don't
|
||||
// toggle it directly — enabling any CRM-area sub-feature
|
||||
// (Accounts today; future Calendar / Quotes / Bills / Messaging)
|
||||
@@ -91,11 +98,12 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|
||||
|| out.crmDevelopment
|
||||
|| out.quotes
|
||||
|| out.bills
|
||||
|| out.taxReport
|
||||
|| out.hoursLogging
|
||||
|| out.contracts
|
||||
// Migration 137 — admin calendar lights up the Clients section.
|
||||
|| out.calendar
|
||||
// NOTE: taxReport is intentionally NOT here anymore — the Tax export
|
||||
// moved permanently into the Accounting section (its own master).
|
||||
// future siblings: || out.messaging
|
||||
);
|
||||
return out;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Wrench,
|
||||
Calculator,
|
||||
Landmark,
|
||||
ScanLine,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Card } from '../../../components/common';
|
||||
@@ -260,39 +261,6 @@ export const FeaturesTab: React.FC = () => {
|
||||
onToggle={(next) => setFlag('bills', next)}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={Calculator}
|
||||
title={t('settings.features.taxReport.title', 'Tax report')}
|
||||
description={t(
|
||||
'settings.features.taxReport.description',
|
||||
'Period-scoped revenue list with net + VAT breakdown grouped by VAT rate. Export as PDF (landscape, company letterhead) or CSV for your accountant. Cancelled invoices stay visible for a gap-free audit trail but are excluded from totals.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.taxReport.sidebar', 'Tax')}
|
||||
enabled={staged.taxReport}
|
||||
onToggle={(next) => setFlag('taxReport', next)}
|
||||
disabled={!staged.bills}
|
||||
lockedReason={!staged.bills ? t(
|
||||
'settings.features.taxReport.requiresBills',
|
||||
'Enable Bills first — the tax report reads from your invoices.',
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={Landmark}
|
||||
title={t('settings.features.accounting.title', 'Accounting')}
|
||||
description={t(
|
||||
'settings.features.accounting.description',
|
||||
'Capture incoming supplier invoices (upload or phone/tablet camera), categorize expenses and re-bill costs to clients on the relevant event. When enabled, the Tax report moves out of CRM into a dedicated Accounting section. VAT / tax treatment is provided as guidance only — verify with your Treuhänder before relying on it.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.accounting.sidebar', 'Accounting')}
|
||||
enabled={staged.accounting}
|
||||
onToggle={(next) => setFlag('accounting', next)}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={Briefcase}
|
||||
title={t('settings.features.hoursLogging.title', 'Hours logging')}
|
||||
@@ -308,6 +276,63 @@ export const FeaturesTab: React.FC = () => {
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Accounting — top-level master + sub-toggles. The Tax export
|
||||
relocated here permanently out of CRM. Sub-toggles are disabled
|
||||
until the Accounting master is on. */}
|
||||
<Section title={t('settings.features.sections.accounting', 'Accounting')}>
|
||||
<FeatureCard
|
||||
icon={Landmark}
|
||||
title={t('settings.features.accounting.title', 'Accounting')}
|
||||
description={t(
|
||||
'settings.features.accounting.description',
|
||||
'A dedicated Accounting area, separate from CRM. Turn this on, then enable the sub-features below (Tax export, Incoming invoices). VAT / tax treatment is guidance only — verify with your Treuhänder before relying on it.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.accounting.sidebar', 'Accounting')}
|
||||
enabled={staged.accounting}
|
||||
onToggle={(next) => setFlag('accounting', next)}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={Calculator}
|
||||
title={t('settings.features.taxReport.title', 'Tax export')}
|
||||
description={t(
|
||||
'settings.features.taxReport.description',
|
||||
'Period-scoped revenue list with net + VAT breakdown grouped by VAT rate. Export as PDF (landscape, company letterhead) or CSV for your accountant. Cancelled invoices stay visible for a gap-free audit trail but are excluded from totals.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.taxReport.sidebar', 'Tax')}
|
||||
enabled={staged.taxReport}
|
||||
onToggle={(next) => setFlag('taxReport', next)}
|
||||
disabled={!staged.accounting}
|
||||
lockedReason={!staged.accounting ? t(
|
||||
'settings.features.taxReport.requiresAccounting',
|
||||
'Enable Accounting first — Tax export lives in the Accounting section.',
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={ScanLine}
|
||||
title={t('settings.features.incomingInvoices.title', 'Incoming invoices')}
|
||||
description={t(
|
||||
'settings.features.incomingInvoices.description',
|
||||
'Capture received supplier invoices (upload or phone/tablet camera), categorize expenses, and re-bill costs to clients on the relevant event with a contract-driven markup.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.incomingInvoices.sidebar', 'Incoming')}
|
||||
enabled={staged.incomingInvoices}
|
||||
onToggle={(next) => setFlag('incomingInvoices', next)}
|
||||
disabled={!staged.accounting}
|
||||
lockedReason={!staged.accounting ? t(
|
||||
'settings.features.incomingInvoices.requiresAccounting',
|
||||
'Enable Accounting first — Incoming invoices live in the Accounting section.',
|
||||
) : undefined}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Insights & Access */}
|
||||
<Section title={t('settings.features.sections.insights', 'Insights & Access')}>
|
||||
<FeatureCard
|
||||
|
||||
@@ -1585,6 +1585,7 @@
|
||||
"communication": "Kommunikation",
|
||||
"scheduling": "Terminplanung",
|
||||
"sales": "Vertrieb",
|
||||
"accounting": "Buchhaltung",
|
||||
"insights": "Auswertungen & Zugriff",
|
||||
"customers": "Kunden",
|
||||
"clients": "CRM"
|
||||
@@ -1631,16 +1632,23 @@
|
||||
"sidebar": "Rechnungen"
|
||||
},
|
||||
"taxReport": {
|
||||
"title": "Steuerliste",
|
||||
"title": "Steuerexport",
|
||||
"description": "Periodenbezogene Umsatzliste mit Netto- und MwSt-Aufschlüsselung, gruppiert nach MwSt-Satz. Exportieren Sie als PDF (Querformat, Firmenbriefkopf) oder CSV für Ihre Buchhaltung. Stornierte Rechnungen bleiben für die lückenlose Nummernfolge sichtbar, sind aber nicht in den Summen enthalten.",
|
||||
"sidebar": "Steuer",
|
||||
"requiresBills": "Bitte zuerst Rechnungen aktivieren — die Steuerliste liest aus Ihren Rechnungen."
|
||||
"requiresBills": "Bitte zuerst Rechnungen aktivieren — die Steuerliste liest aus Ihren Rechnungen.",
|
||||
"requiresAccounting": "Bitte zuerst Buchhaltung aktivieren — der Steuerexport liegt im Buchhaltungsbereich."
|
||||
},
|
||||
"accounting": {
|
||||
"title": "Buchhaltung",
|
||||
"description": "Eingehende Lieferantenrechnungen erfassen (Upload oder Handy-/Tablet-Kamera), Aufwände kategorisieren und Kosten dem passenden Event des Kunden weiterverrechnen. Wenn aktiviert, wandert die Steuerliste aus dem CRM in einen eigenen Buchhaltungsbereich. MwSt-/Steuerbehandlung dient nur als Orientierung — vor dem Verlassen darauf mit Ihrem Treuhänder prüfen.",
|
||||
"description": "Ein eigener Buchhaltungsbereich, getrennt vom CRM. Hier aktivieren und dann die Unterfunktionen unten einschalten (Steuerexport, Eingangsrechnungen). MwSt-/Steuerbehandlung dient nur als Orientierung — vor dem Verlassen darauf mit Ihrem Treuhänder prüfen.",
|
||||
"sidebar": "Buchhaltung"
|
||||
},
|
||||
"incomingInvoices": {
|
||||
"title": "Eingangsrechnungen",
|
||||
"description": "Eingehende Lieferantenrechnungen erfassen (Upload oder Handy-/Tablet-Kamera), Aufwände kategorisieren und Kosten dem passenden Event des Kunden mit vertraglich hinterlegtem Zuschlag weiterverrechnen.",
|
||||
"sidebar": "Eingang",
|
||||
"requiresAccounting": "Bitte zuerst Buchhaltung aktivieren — Eingangsrechnungen liegen im Buchhaltungsbereich."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Statistiken",
|
||||
"description": "Speichernutzung, Galerie-Aufrufe, Download-Zahlen und Statistiken pro Veranstaltung."
|
||||
|
||||
@@ -1143,6 +1143,7 @@
|
||||
"communication": "Communication",
|
||||
"scheduling": "Scheduling",
|
||||
"sales": "Sales",
|
||||
"accounting": "Accounting",
|
||||
"insights": "Insights & Access",
|
||||
"customers": "Customers",
|
||||
"clients": "CRM"
|
||||
@@ -1189,16 +1190,23 @@
|
||||
"sidebar": "Invoices"
|
||||
},
|
||||
"taxReport": {
|
||||
"title": "Tax report",
|
||||
"title": "Tax export",
|
||||
"description": "Period-scoped revenue list with net + VAT breakdown grouped by VAT rate. Export as PDF (landscape, company letterhead) or CSV for your accountant. Cancelled invoices stay visible for a gap-free audit trail but are excluded from totals.",
|
||||
"sidebar": "Tax",
|
||||
"requiresBills": "Enable Bills first — the tax report reads from your invoices."
|
||||
"requiresBills": "Enable Bills first — the tax report reads from your invoices.",
|
||||
"requiresAccounting": "Enable Accounting first — Tax export lives in the Accounting section."
|
||||
},
|
||||
"accounting": {
|
||||
"title": "Accounting",
|
||||
"description": "Capture incoming supplier invoices (upload or phone/tablet camera), categorize expenses and re-bill costs to clients on the relevant event. When enabled, the Tax report moves out of CRM into a dedicated Accounting section. VAT / tax treatment is provided as guidance only — verify with your Treuhänder before relying on it.",
|
||||
"description": "A dedicated Accounting area, separate from CRM. Turn this on, then enable the sub-features below (Tax export, Incoming invoices). VAT / tax treatment is guidance only — verify with your Treuhänder before relying on it.",
|
||||
"sidebar": "Accounting"
|
||||
},
|
||||
"incomingInvoices": {
|
||||
"title": "Incoming invoices",
|
||||
"description": "Capture received supplier invoices (upload or phone/tablet camera), categorize expenses, and re-bill costs to clients on the relevant event with a contract-driven markup.",
|
||||
"sidebar": "Incoming",
|
||||
"requiresAccounting": "Enable Accounting first — Incoming invoices live in the Accounting section."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"description": "Storage usage, gallery views, download counts, and per-event stats."
|
||||
|
||||
@@ -42,11 +42,13 @@ export type FeatureKey =
|
||||
// their own. Seeded block bodies are examples only; admins must
|
||||
// have their lawyer review before sending. See docs/crm-disclaimers.md.
|
||||
| 'contracts'
|
||||
// Accounting (migration 122). Top-level Accounting area — inbound
|
||||
// supplier invoices, expenses + re-bill, plus the tax report, which
|
||||
// relocates here from the CRM sub-nav when this flag is on. Strictly
|
||||
// opt-in; independent of the CRM flags.
|
||||
| 'accounting';
|
||||
// Accounting (migration 122). Top-level MASTER for the Accounting
|
||||
// section (separate from CRM). Its sub-features (tax export, incoming
|
||||
// invoices) require it. Strictly opt-in.
|
||||
| 'accounting'
|
||||
// Incoming invoices (migration 124) — supplier-invoice capture +
|
||||
// expenses + re-bill. Accounting sub-feature; requires `accounting`.
|
||||
| 'incomingInvoices';
|
||||
|
||||
export type FeatureFlags = Record<FeatureKey, boolean>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user