taks-d.2.1

This commit is contained in:
phaichayon
2026-06-17 14:46:52 +07:00
parent 0a484e0b45
commit 5be6c54272
39 changed files with 6254 additions and 316 deletions

View File

@@ -268,16 +268,18 @@ function CustomerDialog({
const [customerId, setCustomerId] = useState(
relation?.customerId ?? referenceData.customers[0]?.id ?? ''
);
const [role, setRole] = useState(relation?.role ?? 'owner');
const [isPrimary, setIsPrimary] = useState(relation?.isPrimary ?? false);
const [role, setRole] = useState(
relation?.role ?? referenceData.projectPartyRoles[0]?.id ?? ''
);
const [remark, setRemark] = useState(relation?.remark ?? '');
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{relation ? 'Edit Related Customer' : 'Add Related Customer'}</DialogTitle>
<DialogTitle>{relation ? 'Edit Project Party' : 'Add Project Party'}</DialogTitle>
<DialogDescription>
Keep owner, consultant, contractor, and billing parties visible on the quote.
Keep related companies visible with their role in this quotation.
</DialogDescription>
</DialogHeader>
<div className='grid gap-4'>
@@ -298,22 +300,14 @@ function CustomerDialog({
<SelectValue placeholder='Role' />
</SelectTrigger>
<SelectContent>
{referenceData.customerRoles.map((item) => (
{referenceData.projectPartyRoles.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<label className='flex items-center justify-between rounded-lg border px-4 py-3 text-sm'>
<span>Primary relation</span>
<input
aria-label='Primary relation'
type='checkbox'
checked={isPrimary}
onChange={(e) => setIsPrimary(e.target.checked)}
/>
</label>
<Input value={remark} onChange={(event) => setRemark(event.target.value)} placeholder='Remark' />
</div>
<DialogFooter>
<Button variant='outline' onClick={() => onOpenChange(false)}>
@@ -322,7 +316,7 @@ function CustomerDialog({
<Button
isLoading={pending}
onClick={async () => {
await onSubmit({ customerId, role, isPrimary });
await onSubmit({ customerId, role, remark: remark || null });
onOpenChange(false);
}}
>
@@ -720,24 +714,24 @@ export function QuotationDetail({
const [deleteCustomerId, setDeleteCustomerId] = useState<string | null>(null);
const createCustomer = useMutation({
...createQuotationCustomerMutation,
onSuccess: () => toast.success('Related customer saved'),
onSuccess: () => toast.success('Project party saved'),
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to save customer')
toast.error(error instanceof Error ? error.message : 'Failed to save project party')
});
const updateCustomer = useMutation({
...updateQuotationCustomerMutation,
onSuccess: () => toast.success('Related customer updated'),
onSuccess: () => toast.success('Project party updated'),
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to update customer')
toast.error(error instanceof Error ? error.message : 'Failed to update project party')
});
const deleteCustomer = useMutation({
...deleteQuotationCustomerMutation,
onSuccess: () => {
toast.success('Related customer deleted');
toast.success('Project party deleted');
setDeleteCustomerId(null);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to delete customer')
toast.error(error instanceof Error ? error.message : 'Failed to delete project party')
});
const [topicOpen, setTopicOpen] = useState(false);
@@ -1137,7 +1131,7 @@ export function QuotationDetail({
<TabsList className='flex flex-wrap'>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='items'>Items</TabsTrigger>
<TabsTrigger value='customers'>Customers</TabsTrigger>
<TabsTrigger value='customers'>Project Parties</TabsTrigger>
<TabsTrigger value='topics'>Topics</TabsTrigger>
<TabsTrigger value='followups'>Follow-ups</TabsTrigger>
<TabsTrigger value='attachments'>Attachments</TabsTrigger>
@@ -1155,7 +1149,7 @@ export function QuotationDetail({
</CardDescription>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<FieldItem label='Customer' value={customer?.name} />
<FieldItem label='Billing Customer' value={customer?.name} />
<FieldItem label='Contact' value={contact?.name} />
<FieldItem
label='Branch'
@@ -1188,6 +1182,28 @@ export function QuotationDetail({
quotation.chancePercent !== null ? `${quotation.chancePercent}%` : null
}
/>
<div className='md:col-span-2 xl:col-span-4'>
<div className='space-y-3'>
<div className='text-muted-foreground text-xs'>Project Parties</div>
{!customersData.items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
No project parties have been added yet.
</div>
) : (
<div className='space-y-3'>
{customersData.items.map((item) => (
<div key={item.id} className='rounded-lg border p-4 text-sm'>
<div className='font-medium'>{item.customerName}</div>
<div className='text-muted-foreground'>
Role: {item.roleLabel ?? item.roleCode}
</div>
{item.remark ? <div className='mt-1'>{item.remark}</div> : null}
</div>
))}
</div>
)}
</div>
</div>
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem
label='Project'
@@ -1285,8 +1301,10 @@ export function QuotationDetail({
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<div>
<CardTitle>Customers</CardTitle>
<CardDescription>All parties involved in this quotation.</CardDescription>
<CardTitle>Project Parties</CardTitle>
<CardDescription>
Related companies for this quotation, each with a visible role.
</CardDescription>
</div>
{canManageCustomers ? (
<Button
@@ -1295,13 +1313,13 @@ export function QuotationDetail({
setCustomerOpen(true);
}}
>
<Icons.add className='mr-2 h-4 w-4' /> Add Customer
<Icons.add className='mr-2 h-4 w-4' /> Add Party
</Button>
) : null}
</CardHeader>
<CardContent className='space-y-3'>
{!customersData.items.length ? (
<EmptyState message='No additional customer relations yet.' />
<EmptyState message='No project parties have been added yet.' />
) : (
customersData.items.map((item) => (
<div
@@ -1311,9 +1329,11 @@ export function QuotationDetail({
<div>
<div className='font-medium'>{item.customerName}</div>
<div className='text-muted-foreground text-sm'>
{item.role}
{item.isPrimary ? ' - Primary' : ''}
Role: {item.roleLabel ?? item.roleCode}
</div>
{item.remark ? (
<div className='text-muted-foreground mt-1 text-sm'>{item.remark}</div>
) : null}
</div>
{canManageCustomers ? (
<div className='flex gap-2'>