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
102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from '@mui/material';
|
|
import { useCreateCustomer } from '../../../hooks/useCustomers';
|
|
import { useSegmentDefinitions } from '../../../hooks/useSegments';
|
|
|
|
export function CustomerDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
const createCustomer = useCreateCustomer();
|
|
const { data: definitions } = useSegmentDefinitions();
|
|
const branchDef = (definitions ?? []).find((def) => def.isBranchSegment);
|
|
|
|
const [code, setCode] = useState('');
|
|
const [name, setName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [phone, setPhone] = useState('');
|
|
const [branchSegmentValueId, setBranchSegmentValueId] = useState<number | ''>('');
|
|
const [creditLimit, setCreditLimit] = useState('');
|
|
const [paymentTermsDays, setPaymentTermsDays] = useState('30');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const reset = () => {
|
|
setCode('');
|
|
setName('');
|
|
setEmail('');
|
|
setPhone('');
|
|
setBranchSegmentValueId('');
|
|
setCreditLimit('');
|
|
setPaymentTermsDays('30');
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
setError(null);
|
|
if (!branchSegmentValueId) {
|
|
setError('Select a branch');
|
|
return;
|
|
}
|
|
try {
|
|
await createCustomer.mutateAsync({
|
|
code,
|
|
name,
|
|
email: email || undefined,
|
|
phone: phone || undefined,
|
|
branchSegmentValueId,
|
|
creditLimit: creditLimit ? Number(creditLimit) : undefined,
|
|
paymentTermsDays: paymentTermsDays ? Number(paymentTermsDays) : undefined,
|
|
});
|
|
onClose();
|
|
reset();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to create customer');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
|
<DialogTitle>New Customer</DialogTitle>
|
|
<DialogContent>
|
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
|
<TextField label="Customer code" value={code} onChange={(e) => setCode(e.target.value)} />
|
|
<TextField label="Name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
<TextField label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
<TextField label="Phone" value={phone} onChange={(e) => setPhone(e.target.value)} />
|
|
<TextField
|
|
select
|
|
label="Branch"
|
|
value={branchSegmentValueId}
|
|
onChange={(e) => setBranchSegmentValueId(Number(e.target.value))}
|
|
>
|
|
{(branchDef?.values ?? []).map((value) => (
|
|
<MenuItem key={value.id} value={value.id}>
|
|
{value.code} — {value.description}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<Stack direction="row" spacing={2}>
|
|
<TextField
|
|
label="Credit limit"
|
|
type="number"
|
|
value={creditLimit}
|
|
onChange={(e) => setCreditLimit(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
<TextField
|
|
label="Payment terms (days)"
|
|
type="number"
|
|
value={paymentTermsDays}
|
|
onChange={(e) => setPaymentTermsDays(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
</Stack>
|
|
{error && <span style={{ color: 'red' }}>{error}</span>}
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onClose}>Cancel</Button>
|
|
<Button variant="contained" onClick={handleSubmit} disabled={createCustomer.isPending}>
|
|
Create
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|