Files
cross-financial-management/client/src/features/ar/customers/CustomerDialog.tsx
T
Joshua OosthuizenandClaude Sonnet 5 4acbc03b35 Initial commit: multi-company financial management system
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
2026-09-03 13:27:19 +02:00

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>
);
}