feat(accounting): move Chart of accounts into Settings → Accounting

Consolidate all accounting configuration in one place. The Chart of
accounts (accounts table + category/default-account mappings) becomes a
self-contained ChartOfAccountsManager rendered in Settings → Accounting,
next to the VAT codes that already moved there. The /admin/accounting
section is now purely operational (Incoming invoices · Expenses · Tax).

The old /admin/accounting/ledger route redirects to the settings tab so
bookmarks keep working; the Tax page "Configure" link points there too.
ChartOfAccountsManager saves only the account keys (partial-merge safe,
same as VatCodesManager), so the two never revert each other's edits.
This commit is contained in:
Luca
2026-06-16 01:11:12 +02:00
parent 4ff5b84cb6
commit 97795f6d1e
5 changed files with 34 additions and 25 deletions
+3 -4
View File
@@ -69,7 +69,6 @@ import { ClientsLayout } from './components/admin/ClientsLayout';
import { AccountingLayout, AccountingIndex } from './components/admin/AccountingLayout';
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage';
import { ChartOfAccountsPage } from './pages/admin/accounting/ChartOfAccountsPage';
import { RequireFeature } from './components/admin/RequireFeature';
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
@@ -291,9 +290,9 @@ function App() {
the old path working for bookmarks. */}
<Route path="export" element={<Navigate to="/admin/accounting/tax-report" replace />} />
</Route>
{/* Chart of accounts + VAT codes (Layer A) — gated by
the accounting master flag alongside the section. */}
<Route path="ledger" element={<ChartOfAccountsPage />} />
{/* Chart of accounts (Layer A) moved into Settings →
Accounting; keep the old path working for bookmarks. */}
<Route path="ledger" element={<Navigate to="/admin/settings?tab=accounting" replace />} />
<Route index element={<AccountingIndex />} />
</Route>
</Route>
@@ -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 } from 'lucide-react';
import { Landmark, Calculator, Inbox, Wallet } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
@@ -52,13 +52,8 @@ export const AccountingLayout: React.FC = () => {
icon: Calculator,
featureFlag: 'taxReport',
},
{
key: 'ledger',
to: '/admin/accounting/ledger',
label: t('accounting.subnav.chartOfAccounts', 'Chart of accounts'),
icon: BookOpen,
featureFlag: 'accounting',
},
// Chart of accounts moved to Settings → Accounting (all accounting config
// lives there now); this section keeps only the operational pages.
// Future: Erfolgsrechnung (Layer B).
];
@@ -1,23 +1,27 @@
/**
* Accounting Chart of accounts (Layer A).
* Chart of accounts manager (Layer A) embedded in Settings Accounting.
*
* Full CRUD for the Swiss/LI KMU-Kontenrahmen accounts, plus the mappings the
* Treuhänder export relies on: which account each expense category books to and
* the default/system accounts. VAT codes + their rate/treatment maps live in
* Settings Accounting (VatCodesManager), so all VAT config sits in one place.
* the default/system accounts. Sits alongside VatCodesManager so all accounting
* configuration lives in one place.
*
* This data drives the export only picpeak is not a double-entry ledger.
*
* NOTE: ledgerService.updateSettings is a PARTIAL merge, so this component saves
* ONLY the account keys (SETTING_ACCOUNT_KEYS); the VAT maps are owned by
* VatCodesManager. Scoping each patch keeps the two from reverting each other.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { X, Plus, Pencil, Trash2, AlertCircle } from 'lucide-react';
import { Button, Card, CardContent, Input, Loading } from '../../../components/common';
import { Button, Card, CardContent, Input, Loading } from '../common';
import {
ledgerService, type LedgerAccount, type AccountType, type LedgerSettings,
} from '../../../services/ledger.service';
import { categoryLabel } from '../../../services/accounting.service';
} from '../../services/ledger.service';
import { categoryLabel } from '../../services/accounting.service';
const ACCOUNT_TYPES: AccountType[] = ['asset', 'liability', 'equity', 'revenue', 'expense'];
const labelCls = 'block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1';
@@ -65,7 +69,7 @@ const AccountModal: React.FC<{ account?: LedgerAccount; onClose: () => void; onD
);
};
export const ChartOfAccountsPage: React.FC = () => {
export const ChartOfAccountsManager: React.FC = () => {
const { t } = useTranslation();
const qc = useQueryClient();
const [accountModal, setAccountModal] = useState<{ account?: LedgerAccount } | null>(null);
@@ -73,7 +77,7 @@ export const ChartOfAccountsPage: React.FC = () => {
const { data: accounts, isLoading: la } = useQuery({ queryKey: ['ledger-accounts'], queryFn: () => ledgerService.listAccounts() });
const { data: mappings, isLoading: lm } = useQuery({ queryKey: ['ledger-mappings'], queryFn: () => ledgerService.getMappings() });
// Local editable copy of the settings (default accounts + VAT maps).
// Local editable copy of the settings (default/system accounts only).
const [settings, setSettings] = useState<LedgerSettings>({});
useEffect(() => { if (mappings?.settings) setSettings(mappings.settings); }, [mappings?.settings]);
@@ -91,9 +95,9 @@ export const ChartOfAccountsPage: React.FC = () => {
onSuccess: () => { qc.invalidateQueries({ queryKey: ['ledger-mappings'] }); },
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'),
});
// Save ONLY the account keys — the VAT maps now live in Settings → Accounting
// (VatCodesManager) and updateSettings is a partial merge, so scoping the
// patch here prevents a stale full-settings save from reverting the maps.
// Save ONLY the account keys — the VAT maps are owned by VatCodesManager and
// updateSettings is a partial merge, so scoping the patch here prevents a
// stale full-settings save from reverting the maps.
const saveSettings = useMutation({
mutationFn: () => {
const patch: Partial<LedgerSettings> = {};
@@ -197,4 +201,4 @@ export const ChartOfAccountsPage: React.FC = () => {
);
};
export default ChartOfAccountsPage;
export default ChartOfAccountsManager;
@@ -13,6 +13,7 @@ import { DecimalInput } from '../../../components/common/DecimalInput';
import { accountingService } from '../../../services/accounting.service';
import { sortedCountryOptions } from '../../../constants/countries';
import { VatCodesManager } from '../../../components/admin/VatCodesManager';
import { ChartOfAccountsManager } from '../../../components/admin/ChartOfAccountsManager';
const labelCls = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1';
const inputCls = 'w-full max-w-xs rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm';
@@ -121,6 +122,16 @@ export const AccountingTab: React.FC = () => {
{/* VAT codes + rate→code / treatment→code maps — relocated here from the
Chart-of-accounts page so all VAT config lives in one place. */}
<VatCodesManager />
{/* Chart of accounts (accounts + category/default-account mappings) —
moved off the /admin/accounting section so all accounting config is
here; the section keeps only the operational pages. */}
<div className="pt-2">
<h3 className="text-sm font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400 mb-3">
{t('ledger.accounts.title', 'Chart of accounts')}
</h3>
<ChartOfAccountsManager />
</div>
</div>
);
};
@@ -346,7 +346,7 @@ export const TaxReportPage: React.FC = () => {
</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">
<Link to="/admin/settings?tab=accounting" className="underline hover:text-neutral-600 dark:hover:text-neutral-300">
{t('taxReport.ledgerExportConfigure', 'Configure →')}
</Link>
</div>