This commit is contained in:
phaichayon
2026-06-16 08:43:19 +07:00
parent eae0dee1f2
commit b8f13e36b3
12 changed files with 547 additions and 21 deletions

View File

@@ -40,6 +40,7 @@ import {
deleteQuotationFollowupMutation,
deleteQuotationItemMutation,
deleteQuotationTopicMutation,
updateQuotationMutation,
updateQuotationAttachmentMutation,
updateQuotationCustomerMutation,
updateQuotationFollowupMutation,
@@ -134,7 +135,15 @@ function ItemDialog({
<Input value={values.description} onChange={(e) => setValues((s) => ({ ...s, description: e.target.value }))} placeholder='Description' />
<div className='grid gap-4 md:grid-cols-2'>
<Input value={values.quantity} onChange={(e) => setValues((s) => ({ ...s, quantity: e.target.value }))} type='number' placeholder='Quantity' />
<Input value={values.unit} onChange={(e) => setValues((s) => ({ ...s, unit: e.target.value }))} placeholder='Unit' />
<Select value={values.unit || '__none__'} onValueChange={(value) => setValues((s) => ({ ...s, unit: value === '__none__' ? '' : value }))}>
<SelectTrigger><SelectValue placeholder='Unit' /></SelectTrigger>
<SelectContent>
<SelectItem value='__none__'>No unit</SelectItem>
{referenceData.units.map((option) => (
<SelectItem key={option.id} value={option.id}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-4 md:grid-cols-2'>
<Input value={values.unitPrice} onChange={(e) => setValues((s) => ({ ...s, unitPrice: e.target.value }))} type='number' placeholder='Unit price' />
@@ -219,8 +228,8 @@ function CustomerDialog({
<Select value={role} onValueChange={setRole}>
<SelectTrigger><SelectValue placeholder='Role' /></SelectTrigger>
<SelectContent>
{['owner', 'consultant', 'contractor', 'billing'].map((item) => (
<SelectItem key={item} value={item}>{item}</SelectItem>
{referenceData.customerRoles.map((item) => (
<SelectItem key={item.id} value={item.id}>{item.label}</SelectItem>
))}
</SelectContent>
</Select>
@@ -601,6 +610,12 @@ export function QuotationDetail({
onSuccess: () => toast.success('Revision created successfully'),
onError: (error) => toast.error(error instanceof Error ? error.message : 'Failed to create revision')
});
const submitForApproval = useMutation({
...updateQuotationMutation,
onSuccess: () => toast.success('Quotation moved to pending approval'),
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to submit for approval')
});
const branchMap = useMemo(
() => new Map(referenceData.branches.map((item) => [item.id, item.name])),
@@ -637,6 +652,10 @@ export function QuotationDetail({
const customer = customerMap.get(quotation.customerId);
const contact = quotation.contactId ? contactMap.get(quotation.contactId) : null;
const status = statusMap.get(quotation.status);
const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
status?.code ?? ''
);
const canSubmitApproval = ['draft', 'revised'].includes(status?.code ?? '');
return (
<div className='space-y-6'>
@@ -795,7 +814,49 @@ export function QuotationDetail({
</div>
</div>
<div className='flex flex-wrap gap-2'>
{canCreateRevision ? (
{canUpdate && canSubmitApproval ? (
<Button
variant='outline'
isLoading={submitForApproval.isPending}
onClick={() =>
submitForApproval.mutate({
id: quotationId,
values: {
enquiryId: quotation.enquiryId,
customerId: quotation.customerId,
contactId: quotation.contactId,
quotationDate: quotation.quotationDate,
validUntil: quotation.validUntil,
quotationType: quotation.quotationType,
projectName: quotation.projectName ?? '',
projectLocation: quotation.projectLocation ?? '',
attention: quotation.attention ?? '',
branchId: quotation.branchId,
currency: quotation.currency,
exchangeRate: quotation.exchangeRate,
status:
referenceData.statuses.find((item) => item.code === 'pending_approval')?.id ??
quotation.status,
chancePercent: quotation.chancePercent,
isHotProject: quotation.isHotProject,
competitor: quotation.competitor ?? '',
reference: quotation.reference ?? '',
notes: quotation.notes ?? '',
salesmanId: quotation.salesmanId,
discount: quotation.discount,
discountType: quotation.discountType,
taxRate: quotation.taxRate,
sentVia: quotation.sentVia,
revisionRemark: quotation.revisionRemark,
isActive: quotation.isActive
}
})
}
>
<Icons.send className='mr-2 h-4 w-4' /> Submit for approval
</Button>
) : null}
{canCreateRevision && canReviseByStatus ? (
<Button
variant='outline'
isLoading={createRevision.isPending}