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:
@@ -69,7 +69,6 @@ import { ClientsLayout } from './components/admin/ClientsLayout';
|
|||||||
import { AccountingLayout, AccountingIndex } from './components/admin/AccountingLayout';
|
import { AccountingLayout, AccountingIndex } from './components/admin/AccountingLayout';
|
||||||
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
|
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
|
||||||
import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage';
|
import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage';
|
||||||
import { ChartOfAccountsPage } from './pages/admin/accounting/ChartOfAccountsPage';
|
|
||||||
import { RequireFeature } from './components/admin/RequireFeature';
|
import { RequireFeature } from './components/admin/RequireFeature';
|
||||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
|
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
|
||||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||||
@@ -291,9 +290,9 @@ function App() {
|
|||||||
the old path working for bookmarks. */}
|
the old path working for bookmarks. */}
|
||||||
<Route path="export" element={<Navigate to="/admin/accounting/tax-report" replace />} />
|
<Route path="export" element={<Navigate to="/admin/accounting/tax-report" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
{/* Chart of accounts + VAT codes (Layer A) — gated by
|
{/* Chart of accounts (Layer A) moved into Settings →
|
||||||
the accounting master flag alongside the section. */}
|
Accounting; keep the old path working for bookmarks. */}
|
||||||
<Route path="ledger" element={<ChartOfAccountsPage />} />
|
<Route path="ledger" element={<Navigate to="/admin/settings?tab=accounting" replace />} />
|
||||||
<Route index element={<AccountingIndex />} />
|
<Route index element={<AccountingIndex />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 type { LucideIcon } from 'lucide-react';
|
||||||
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
||||||
|
|
||||||
@@ -52,13 +52,8 @@ export const AccountingLayout: React.FC = () => {
|
|||||||
icon: Calculator,
|
icon: Calculator,
|
||||||
featureFlag: 'taxReport',
|
featureFlag: 'taxReport',
|
||||||
},
|
},
|
||||||
{
|
// Chart of accounts moved to Settings → Accounting (all accounting config
|
||||||
key: 'ledger',
|
// lives there now); this section keeps only the operational pages.
|
||||||
to: '/admin/accounting/ledger',
|
|
||||||
label: t('accounting.subnav.chartOfAccounts', 'Chart of accounts'),
|
|
||||||
icon: BookOpen,
|
|
||||||
featureFlag: 'accounting',
|
|
||||||
},
|
|
||||||
// Future: Erfolgsrechnung (Layer B).
|
// Future: Erfolgsrechnung (Layer B).
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -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
|
* 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
|
* 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
|
* the default/system accounts. Sits alongside VatCodesManager so all accounting
|
||||||
* Settings → Accounting (VatCodesManager), so all VAT config sits in one place.
|
* configuration lives in one place.
|
||||||
*
|
*
|
||||||
* This data drives the export only — picpeak is not a double-entry ledger.
|
* 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 React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { X, Plus, Pencil, Trash2, AlertCircle } from 'lucide-react';
|
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 {
|
import {
|
||||||
ledgerService, type LedgerAccount, type AccountType, type LedgerSettings,
|
ledgerService, type LedgerAccount, type AccountType, type LedgerSettings,
|
||||||
} from '../../../services/ledger.service';
|
} from '../../services/ledger.service';
|
||||||
import { categoryLabel } from '../../../services/accounting.service';
|
import { categoryLabel } from '../../services/accounting.service';
|
||||||
|
|
||||||
const ACCOUNT_TYPES: AccountType[] = ['asset', 'liability', 'equity', 'revenue', 'expense'];
|
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';
|
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 { t } = useTranslation();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [accountModal, setAccountModal] = useState<{ account?: LedgerAccount } | null>(null);
|
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: accounts, isLoading: la } = useQuery({ queryKey: ['ledger-accounts'], queryFn: () => ledgerService.listAccounts() });
|
||||||
const { data: mappings, isLoading: lm } = useQuery({ queryKey: ['ledger-mappings'], queryFn: () => ledgerService.getMappings() });
|
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>({});
|
const [settings, setSettings] = useState<LedgerSettings>({});
|
||||||
useEffect(() => { if (mappings?.settings) setSettings(mappings.settings); }, [mappings?.settings]);
|
useEffect(() => { if (mappings?.settings) setSettings(mappings.settings); }, [mappings?.settings]);
|
||||||
|
|
||||||
@@ -91,9 +95,9 @@ export const ChartOfAccountsPage: React.FC = () => {
|
|||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['ledger-mappings'] }); },
|
onSuccess: () => { qc.invalidateQueries({ queryKey: ['ledger-mappings'] }); },
|
||||||
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'),
|
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
|
// Save ONLY the account keys — the VAT maps are owned by VatCodesManager and
|
||||||
// (VatCodesManager) and updateSettings is a partial merge, so scoping the
|
// updateSettings is a partial merge, so scoping the patch here prevents a
|
||||||
// patch here prevents a stale full-settings save from reverting the maps.
|
// stale full-settings save from reverting the maps.
|
||||||
const saveSettings = useMutation({
|
const saveSettings = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
const patch: Partial<LedgerSettings> = {};
|
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 { accountingService } from '../../../services/accounting.service';
|
||||||
import { sortedCountryOptions } from '../../../constants/countries';
|
import { sortedCountryOptions } from '../../../constants/countries';
|
||||||
import { VatCodesManager } from '../../../components/admin/VatCodesManager';
|
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 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';
|
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
|
{/* VAT codes + rate→code / treatment→code maps — relocated here from the
|
||||||
Chart-of-accounts page so all VAT config lives in one place. */}
|
Chart-of-accounts page so all VAT config lives in one place. */}
|
||||||
<VatCodesManager />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-neutral-400 dark:text-neutral-500">
|
<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.')}{' '}
|
{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 →')}
|
{t('taxReport.ledgerExportConfigure', 'Configure →')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user