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:
Luca
2026-06-11 00:17:44 +02:00
parent 30c0007f40
commit 2c351bf0c9
10 changed files with 157 additions and 88 deletions
@@ -1,22 +1,39 @@
/** /**
* Migration 122: seed the `accounting` feature flag (default OFF). * Migration 122: seed the Accounting feature flags.
* *
* Gates the new top-level Accounting area (inbound supplier invoices, * - `accounting` : top-level MASTER for the Accounting section
* billable / re-billable expenses, Erfolgsrechnung). Admins opt in under * (separate from CRM). Default OFF — EXCEPT on
* Settings → Features. Separate from the CRM `bills` flag. * installs that already had the Tax report
* (`taxReport`) enabled: the Tax export relocated
* permanently into Accounting, so we auto-enable the
* master there to preserve the existing menu (per the
* "migrations preserve visual state" rule). Otherwise
* admins opt in under Settings → Features.
* - `incomingInvoices` : Accounting sub-feature (supplier-invoice capture +
* expenses + re-bill). Always default OFF (new).
* *
* Idempotent: inserts only when the row is missing (mirrors the * Idempotent: each row is inserted only when missing. 107_crm_consolidated
* 095_add_customer_portal_flag pattern). 107_crm_consolidated already * already shipped its flag set and won't re-run.
* shipped its flag set on fresh installs and won't re-run.
*/ */
exports.up = async function (knex) { exports.up = async function (knex) {
if (!(await knex.schema.hasTable('feature_flags'))) return; if (!(await knex.schema.hasTable('feature_flags'))) return;
const existing = await knex('feature_flags').where({ key: 'accounting' }).first();
if (existing) return; const existingAccounting = await knex('feature_flags').where({ key: 'accounting' }).first();
await knex('feature_flags').insert({ key: 'accounting', value: false }); if (!existingAccounting) {
// Preserve visuals: if Tax was already on, light up the Accounting
// master so the relocated Tax export doesn't vanish on upgrade.
const taxRow = await knex('feature_flags').where({ key: 'taxReport' }).first();
const taxOn = !!(taxRow && (taxRow.value === true || taxRow.value === 1 || taxRow.value === '1'));
await knex('feature_flags').insert({ key: 'accounting', value: taxOn });
}
const existingIncoming = await knex('feature_flags').where({ key: 'incomingInvoices' }).first();
if (!existingIncoming) {
await knex('feature_flags').insert({ key: 'incomingInvoices', value: false });
}
}; };
exports.down = async function (knex) { exports.down = async function (knex) {
if (!(await knex.schema.hasTable('feature_flags'))) return; if (!(await knex.schema.hasTable('feature_flags'))) return;
await knex('feature_flags').where({ key: 'accounting' }).del(); await knex('feature_flags').whereIn('key', ['accounting', 'incomingInvoices']).del();
}; };
+7 -4
View File
@@ -43,17 +43,20 @@ const inboundUpload = multer({
}, },
}); });
async function requireAccountingFlag(req, res, next) { // Gated on the `incomingInvoices` sub-feature, which the feature-flag
// dependency rules force OFF whenever the `accounting` master is off — so
// this single check covers both.
async function requireIncomingInvoicesFlag(req, res, next) {
try { try {
const row = await db('feature_flags').where({ key: 'accounting' }).first(); const row = await db('feature_flags').where({ key: 'incomingInvoices' }).first();
const enabled = row && (row.value === true || row.value === 1 || row.value === '1'); const enabled = row && (row.value === true || row.value === 1 || row.value === '1');
if (!enabled) return res.status(403).json({ error: 'Accounting feature is disabled', code: 'ACCOUNTING_DISABLED' }); if (!enabled) return res.status(403).json({ error: 'Incoming invoices feature is disabled', code: 'INCOMING_INVOICES_DISABLED' });
return next(); return next();
} catch (err) { return next(err); } } catch (err) { return next(err); }
} }
router.use(adminAuth); router.use(adminAuth);
router.use(requireAccountingFlag); router.use(requireIncomingInvoicesFlag);
// ── Expense categories (literal path — register BEFORE '/:id') ────────────── // ── Expense categories (literal path — register BEFORE '/:id') ──────────────
router.get('/categories', requirePermission('accounting.view'), handleAsync(async (_req, res) => { router.get('/categories', requirePermission('accounting.view'), handleAsync(async (_req, res) => {
+14 -5
View File
@@ -66,6 +66,10 @@ const KNOWN_FLAGS = [
// supplier invoices, expenses + re-bill, and the tax report (which // supplier invoices, expenses + re-bill, and the tax report (which
// relocates here from CRM when this is on). Strictly opt-in. // relocates here from CRM when this is on). Strictly opt-in.
'accounting', 'accounting',
// Incoming invoices (migration 124) — supplier-invoice capture +
// expenses + re-bill. Accounting sub-feature; forced off when the
// `accounting` master is off.
'incomingInvoices',
]; ];
// Spec defaults for any flag missing from the DB (e.g. a row added by a // Spec defaults for any flag missing from the DB (e.g. a row added by a
@@ -89,6 +93,7 @@ const DEFAULT_FLAGS = {
hoursLogging: false, hoursLogging: false,
contracts: false, contracts: false,
accounting: false, accounting: false,
incomingInvoices: false,
}; };
async function readAllFlags() { async function readAllFlags() {
@@ -109,10 +114,13 @@ function applyDependencyRules(flags) {
// Sub-features can't outlive their parents. // Sub-features can't outlive their parents.
if (out.quotes === false) out.bills = false; if (out.quotes === false) out.bills = false;
if (out.calendar === false) out.calendarBooking = false; if (out.calendar === false) out.calendarBooking = false;
// Tax report only makes sense when bills are on — turning bills off // Accounting is a top-level MASTER; its sub-features can't outlive it.
// implicitly turns the tax report off too. Admins enabling tax // Tax export is now independent of Bills — it relocated permanently
// report must first enable bills. // into the Accounting section (its own master gate).
if (out.bills === false) out.taxReport = false; if (out.accounting === false) {
out.taxReport = false;
out.incomingInvoices = false;
}
// Clients parent flag is DERIVED from its children. Admins don't // Clients parent flag is DERIVED from its children. Admins don't
// toggle it directly in the Features tab — they enable a specific // toggle it directly in the Features tab — they enable a specific
// sub-feature (Accounts today; Calendar/Quotes/Bills/Messaging // sub-feature (Accounts today; Calendar/Quotes/Bills/Messaging
@@ -125,9 +133,10 @@ function applyDependencyRules(flags) {
|| out.crmDevelopment || out.crmDevelopment
|| out.quotes || out.quotes
|| out.bills || out.bills
|| out.taxReport
|| out.hoursLogging || out.hoursLogging
|| out.contracts || out.contracts
// NOTE: taxReport intentionally removed — Tax export moved to the
// Accounting section (its own master), no longer a CRM sub-feature.
// Migration 137 — admin calendar lights up the Clients section. // Migration 137 — admin calendar lights up the Clients section.
// (calendarBooking is gated behind `calendar` so adding the parent // (calendarBooking is gated behind `calendar` so adding the parent
// is sufficient.) // is sufficient.)
+4 -4
View File
@@ -244,10 +244,10 @@ function App() {
/> />
</Route> </Route>
{/* Tax / Steuer report — gated by `taxReport`. */} {/* Tax export moved permanently to the Accounting
<Route element={<RequireFeature flag="taxReport" />}> section. Keep this path as a redirect so old
<Route path="tax-report" element={<TaxReportPage />} /> bookmarks / links don't 404. */}
</Route> <Route path="tax-report" element={<Navigate to="/admin/accounting/tax-report" replace />} />
{/* Developer tools — gated by `crmDevelopment`. */} {/* Developer tools — gated by `crmDevelopment`. */}
<Route element={<RequireFeature flag="crmDevelopment" />}> <Route element={<RequireFeature flag="crmDevelopment" />}>
<Route path="development" element={<CrmDevelopmentPage />} /> <Route path="development" element={<CrmDevelopmentPage />} />
@@ -14,7 +14,7 @@
import React from 'react'; import React from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; 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 type { LucideIcon } from 'lucide-react';
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext'; import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
@@ -81,13 +81,8 @@ export const ClientsLayout: React.FC = () => {
icon: Receipt, icon: Receipt,
featureFlag: 'bills', featureFlag: 'bills',
}, },
{ // Tax export moved permanently to the Accounting section (it is no
key: 'tax-report', // longer a CRM sub-feature). See AccountingLayout.
to: '/admin/clients/tax-report',
label: t('clients.subnav.taxReport', 'Tax'),
icon: Calculator,
featureFlag: 'taxReport',
},
// Future sub-features: // Future sub-features:
// { key: 'messaging', ... featureFlag: 'messaging' } // { key: 'messaging', ... featureFlag: 'messaging' }
{ {
@@ -99,13 +94,7 @@ export const ClientsLayout: React.FC = () => {
}, },
]; ];
const enabledItems = navItems.filter((item) => { const enabledItems = navItems.filter((item) => flags[item.featureFlag]);
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;
});
// When the parent `clients` flag is on but no sub-feature is enabled, // When the parent `clients` flag is on but no sub-feature is enabled,
// there's nothing to render. Settings → Features is one click away // there's nothing to render. Settings → Features is one click away
+13 -5
View File
@@ -45,10 +45,12 @@ export const DEFAULT_FLAGS: FeatureFlags = {
// Settings → Features once they've reviewed the seeded block // Settings → Features once they've reviewed the seeded block
// library with their lawyer. // library with their lawyer.
contracts: false, contracts: false,
// Accounting (migration 122). Top-level Accounting area (inbound // Accounting (migration 122). Top-level MASTER for the Accounting
// supplier invoices, expenses + re-bill). When ON, the tax report // section (separate from CRM). Sub-features below require it.
// moves out of the CRM sub-nav and under Accounting. Strictly opt-in.
accounting: false, 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; 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 out.galleries = true; // foundation — always on
if (out.quotes === false) out.bills = false; // bills depend on quotes if (out.quotes === false) out.bills = false; // bills depend on quotes
if (out.calendar === false) out.calendarBooking = false; // booking depends on calendar 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 // Clients parent flag is DERIVED from its children. Admins don't
// toggle it directly — enabling any CRM-area sub-feature // toggle it directly — enabling any CRM-area sub-feature
// (Accounts today; future Calendar / Quotes / Bills / Messaging) // (Accounts today; future Calendar / Quotes / Bills / Messaging)
@@ -91,11 +98,12 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|| out.crmDevelopment || out.crmDevelopment
|| out.quotes || out.quotes
|| out.bills || out.bills
|| out.taxReport
|| out.hoursLogging || out.hoursLogging
|| out.contracts || out.contracts
// Migration 137 — admin calendar lights up the Clients section. // Migration 137 — admin calendar lights up the Clients section.
|| out.calendar || 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 // future siblings: || out.messaging
); );
return out; return out;
@@ -17,6 +17,7 @@ import {
Wrench, Wrench,
Calculator, Calculator,
Landmark, Landmark,
ScanLine,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Card } from '../../../components/common'; import { Button, Card } from '../../../components/common';
@@ -260,39 +261,6 @@ export const FeaturesTab: React.FC = () => {
onToggle={(next) => setFlag('bills', next)} 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 <FeatureCard
icon={Briefcase} icon={Briefcase}
title={t('settings.features.hoursLogging.title', 'Hours logging')} title={t('settings.features.hoursLogging.title', 'Hours logging')}
@@ -308,6 +276,63 @@ export const FeaturesTab: React.FC = () => {
/> />
</Section> </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 */} {/* Insights & Access */}
<Section title={t('settings.features.sections.insights', 'Insights & Access')}> <Section title={t('settings.features.sections.insights', 'Insights & Access')}>
<FeatureCard <FeatureCard
+11 -3
View File
@@ -1585,6 +1585,7 @@
"communication": "Kommunikation", "communication": "Kommunikation",
"scheduling": "Terminplanung", "scheduling": "Terminplanung",
"sales": "Vertrieb", "sales": "Vertrieb",
"accounting": "Buchhaltung",
"insights": "Auswertungen & Zugriff", "insights": "Auswertungen & Zugriff",
"customers": "Kunden", "customers": "Kunden",
"clients": "CRM" "clients": "CRM"
@@ -1631,16 +1632,23 @@
"sidebar": "Rechnungen" "sidebar": "Rechnungen"
}, },
"taxReport": { "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.", "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", "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": { "accounting": {
"title": "Buchhaltung", "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" "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": { "analytics": {
"title": "Statistiken", "title": "Statistiken",
"description": "Speichernutzung, Galerie-Aufrufe, Download-Zahlen und Statistiken pro Veranstaltung." "description": "Speichernutzung, Galerie-Aufrufe, Download-Zahlen und Statistiken pro Veranstaltung."
+11 -3
View File
@@ -1143,6 +1143,7 @@
"communication": "Communication", "communication": "Communication",
"scheduling": "Scheduling", "scheduling": "Scheduling",
"sales": "Sales", "sales": "Sales",
"accounting": "Accounting",
"insights": "Insights & Access", "insights": "Insights & Access",
"customers": "Customers", "customers": "Customers",
"clients": "CRM" "clients": "CRM"
@@ -1189,16 +1190,23 @@
"sidebar": "Invoices" "sidebar": "Invoices"
}, },
"taxReport": { "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.", "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", "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": { "accounting": {
"title": "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" "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": { "analytics": {
"title": "Analytics", "title": "Analytics",
"description": "Storage usage, gallery views, download counts, and per-event stats." "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 // their own. Seeded block bodies are examples only; admins must
// have their lawyer review before sending. See docs/crm-disclaimers.md. // have their lawyer review before sending. See docs/crm-disclaimers.md.
| 'contracts' | 'contracts'
// Accounting (migration 122). Top-level Accounting area — inbound // Accounting (migration 122). Top-level MASTER for the Accounting
// supplier invoices, expenses + re-bill, plus the tax report, which // section (separate from CRM). Its sub-features (tax export, incoming
// relocates here from the CRM sub-nav when this flag is on. Strictly // invoices) require it. Strictly opt-in.
// opt-in; independent of the CRM flags. | 'accounting'
| 'accounting'; // Incoming invoices (migration 124) — supplier-invoice capture +
// expenses + re-bill. Accounting sub-feature; requires `accounting`.
| 'incomingInvoices';
export type FeatureFlags = Record<FeatureKey, boolean>; export type FeatureFlags = Record<FeatureKey, boolean>;