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
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from '@mui/material';
|
|
import { useCreateTaxCode } from '../../../hooks/useTaxCodes';
|
|
|
|
export function TaxCodeDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
const createTaxCode = useCreateTaxCode();
|
|
const [code, setCode] = useState('');
|
|
const [name, setName] = useState('');
|
|
const [ratePercent, setRatePercent] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async () => {
|
|
setError(null);
|
|
try {
|
|
await createTaxCode.mutateAsync({ code, name, ratePercent: Number(ratePercent) });
|
|
onClose();
|
|
setCode('');
|
|
setName('');
|
|
setRatePercent('');
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to create tax code');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
|
<DialogTitle>New Tax Code</DialogTitle>
|
|
<DialogContent>
|
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
|
<TextField label="Code" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} helperText="e.g. STD, ZERO" />
|
|
<TextField label="Name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
<TextField
|
|
label="Rate %"
|
|
type="number"
|
|
value={ratePercent}
|
|
onChange={(e) => setRatePercent(e.target.value)}
|
|
slotProps={{ htmlInput: { min: 0, max: 100, step: '0.001' } }}
|
|
/>
|
|
{error && <span style={{ color: 'red' }}>{error}</span>}
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onClose}>Cancel</Button>
|
|
<Button variant="contained" onClick={handleSubmit} disabled={createTaxCode.isPending}>
|
|
Create
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|