Root company app (src/, client/) — GL foundation, AR, and AP: - Segmented Chart of Accounts (Sage 300-style), double-entry PostingService with three-layer balance enforcement (app validation, CHECK constraints, deferred DB trigger), gap-free document numbering, RBAC with branch restrictions. - Accounts Receivable: Customers, Invoices, Credit Notes, Receipts with allocation, all posting through PostingService. - Accounts Payable: Suppliers, Supplier Invoices, Debit Notes, Payments with allocation, mirroring AR's posting pattern. - Tax Codes shared between AR and AP. - Company-app login is a token handoff from the Account Manager (RS256, single-use, company-scoped) rather than a local password. - Frontend reskinned to the approved mockup: navy sidebar, dashboard, tree-view Chart of Accounts, Customer/Supplier 360 pages. Account Manager (manager/, manager/client/) — central login gateway: - Password + TOTP 2FA + Google/Microsoft OAuth, company picker, and the RS256 handoff token issuance that the company app verifies. Covered by unit + integration tests against a real Postgres database (GL balance invariants, AR/AP posting correctness and allocation limits, the full handoff exchange including replay/cross-company/expiry/forgery rejection). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tq2DbEvzHzs13jDFhLaX44
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Box, Button, Paper, Stack, TextField, Typography } from '@mui/material';
|
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
|
import { useCreateFiscalYear, useFiscalYears } from '../../../hooks/useFiscalYears';
|
|
import { StatusChip } from '../../../components/common/StatusChip';
|
|
import type { FiscalPeriod } from '../../../types/api';
|
|
|
|
export function AdminFiscalYearsPage() {
|
|
const { data: fiscalYears, isLoading } = useFiscalYears();
|
|
const createFiscalYear = useCreateFiscalYear();
|
|
const [newYear, setNewYear] = useState('');
|
|
const [selectedYearId, setSelectedYearId] = useState<number | null>(null);
|
|
|
|
const selectedYear = (fiscalYears ?? []).find((year) => year.id === selectedYearId) ?? fiscalYears?.[0];
|
|
|
|
const periodColumns: GridColDef<FiscalPeriod>[] = [
|
|
{ field: 'periodNumber', headerName: 'Period', width: 100 },
|
|
{ field: 'startDate', headerName: 'Start', width: 140, valueFormatter: (value: string) => value?.slice(0, 10) },
|
|
{ field: 'endDate', headerName: 'End', width: 140, valueFormatter: (value: string) => value?.slice(0, 10) },
|
|
{ field: 'status', headerName: 'Status', width: 120, renderCell: (params) => <StatusChip status={params.row.status} /> },
|
|
];
|
|
|
|
return (
|
|
<Box>
|
|
<Typography variant="h5" sx={{ mb: 2 }}>
|
|
Fiscal Years
|
|
</Typography>
|
|
<Stack direction="row" spacing={2} sx={{ mb: 3 }}>
|
|
<TextField label="New fiscal year" type="number" size="small" value={newYear} onChange={(e) => setNewYear(e.target.value)} />
|
|
<Button
|
|
variant="contained"
|
|
disabled={!newYear || createFiscalYear.isPending}
|
|
onClick={async () => {
|
|
await createFiscalYear.mutateAsync(Number(newYear));
|
|
setNewYear('');
|
|
}}
|
|
>
|
|
Create Year (12 periods)
|
|
</Button>
|
|
</Stack>
|
|
|
|
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
|
|
{(fiscalYears ?? []).map((year) => (
|
|
<Button
|
|
key={year.id}
|
|
size="small"
|
|
variant={year.id === selectedYear?.id ? 'contained' : 'outlined'}
|
|
onClick={() => setSelectedYearId(year.id)}
|
|
>
|
|
{year.year}
|
|
</Button>
|
|
))}
|
|
</Stack>
|
|
|
|
{selectedYear && (
|
|
<Paper>
|
|
<Box sx={{ height: 500 }}>
|
|
<DataGrid
|
|
rows={selectedYear.periods}
|
|
columns={periodColumns}
|
|
loading={isLoading}
|
|
density="compact"
|
|
disableRowSelectionOnClick
|
|
/>
|
|
</Box>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|