Files
alla-allaos-fullstack/src/features/crm-demo/components/quotation-detail.tsx
phaichayon b8cd39eaa4 task-b
task-b.1
complere
2026-06-15 11:19:31 +07:00

346 lines
12 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { quotationByIdOptions } from '../api/queries';
import {
approveQuotationMutation,
createQuotationRevisionMutation,
markQuotationStatusMutation,
rejectQuotationMutation,
submitApprovalMutation
} from '../api/mutations';
import {
AuditTimeline,
CustomerRoleCards,
InfoGrid,
QuotationPreviewPanel,
QuotationStatusBadge,
SectionCard,
TimelineList
} from './shared';
import { formatCurrency } from '../utils/format';
function ApprovalDialog({
title,
description,
actionLabel,
onConfirm,
isPending
}: {
title: string;
description: string;
actionLabel: string;
onConfirm: (comment: string) => void;
isPending: boolean;
}) {
const [comment, setComment] = useState('');
return (
<Dialog>
<DialogTrigger asChild>
<Button variant='outline'>{actionLabel}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Textarea
value={comment}
onChange={(event) => setComment(event.target.value)}
placeholder='ใส่หมายเหตุ (optional)'
/>
<DialogFooter>
<Button isLoading={isPending} onClick={() => onConfirm(comment)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function QuotationDetailPage({ id }: { id: string }) {
const { data } = useSuspenseQuery(quotationByIdOptions(id));
const baseSubmitApproval = submitApprovalMutation;
const baseApproveMutation = approveQuotationMutation;
const baseRejectMutation = rejectQuotationMutation;
const baseReviseMutation = createQuotationRevisionMutation;
const baseMarkStatusMutation = markQuotationStatusMutation;
const submitApproval = useMutation({
...baseSubmitApproval,
onSuccess: async (...args) => {
await baseSubmitApproval.onSuccess?.(...args);
toast.success('ส่งเข้าสายอนุมัติแล้ว');
}
});
const approveMutation = useMutation({
...baseApproveMutation,
onSuccess: async (...args) => {
await baseApproveMutation.onSuccess?.(...args);
toast.success('อนุมัติ mock สำเร็จ');
}
});
const rejectMutation = useMutation({
...baseRejectMutation,
onSuccess: async (...args) => {
await baseRejectMutation.onSuccess?.(...args);
toast.success('Reject mock สำเร็จ');
}
});
const reviseMutation = useMutation({
...baseReviseMutation,
onSuccess: async (...args) => {
await baseReviseMutation.onSuccess?.(...args);
toast.success('สร้าง revision mock สำเร็จ');
}
});
const markStatusMutation = useMutation({
...baseMarkStatusMutation,
onSuccess: async (...args) => {
await baseMarkStatusMutation.onSuccess?.(...args);
toast.success('อัปเดตสถานะ mock สำเร็จ');
}
});
const ownerCustomer = data.customers.find((customer) =>
data.quotation.customerRoles.some(
(role) => role.role === 'owner' && role.customerId === customer.id
)
);
const ownerContact = data.contacts.find((contact) =>
data.quotation.customerRoles.some(
(role) => role.role === 'owner' && role.contactId === contact.id
)
);
return (
<div className='space-y-4'>
<SectionCard
title={data.quotation.project}
description={`${data.quotation.code} / ${data.branch.name}`}
action={
<div className='flex flex-wrap gap-2'>
<Button
variant='outline'
isLoading={submitApproval.isPending}
onClick={() => submitApproval.mutate(data.quotation.id)}
>
Submit approval
</Button>
<ApprovalDialog
title='Approve quotation'
description='Mock sequential approval for demo.'
actionLabel='Approve'
isPending={approveMutation.isPending}
onConfirm={(comment) =>
approveMutation.mutate({ quotationId: data.quotation.id, comment })
}
/>
<ApprovalDialog
title='Reject quotation'
description='Mock reject flow for demo.'
actionLabel='Reject'
isPending={rejectMutation.isPending}
onConfirm={(comment) =>
rejectMutation.mutate({ quotationId: data.quotation.id, comment })
}
/>
<Button
variant='outline'
isLoading={reviseMutation.isPending}
onClick={() => reviseMutation.mutate(data.quotation.id)}
>
Create revision
</Button>
<Button
variant='outline'
isLoading={markStatusMutation.isPending}
onClick={() =>
markStatusMutation.mutate({
quotationId: data.quotation.id,
status: 'sent'
})
}
>
Send to customer
</Button>
<Button
variant='outline'
isLoading={markStatusMutation.isPending}
onClick={() =>
markStatusMutation.mutate({
quotationId: data.quotation.id,
status: 'accepted'
})
}
>
Mark accepted
</Button>
<Button
variant='outline'
isLoading={markStatusMutation.isPending}
onClick={() =>
markStatusMutation.mutate({
quotationId: data.quotation.id,
status: 'lost'
})
}
>
Mark lost
</Button>
</div>
}
>
<div className='flex flex-wrap items-center gap-3'>
<QuotationStatusBadge status={data.quotation.status} />
<span className='text-muted-foreground text-sm'>
Revision R{String(data.quotation.revision).padStart(2, '0')}
</span>
<span className='text-sm font-medium'>{formatCurrency(data.quotation.totalAmount)}</span>
</div>
<div className='mt-4'>
<InfoGrid
cols={4}
items={[
{ label: 'Quotation Date', value: data.quotation.quotationDate },
{ label: 'Valid Until', value: data.quotation.validUntil },
{ label: 'Type', value: data.quotation.quotationType },
{ label: 'Chance', value: `${data.quotation.chancePercent}%` },
{ label: 'Salesman', value: data.salesman.name },
{ label: 'Sales Admin', value: data.saleAdmin.name },
{ label: 'Branch', value: data.branch.name },
{ label: 'Site', value: data.quotation.siteLocation }
]}
/>
</div>
</SectionCard>
<SectionCard title='Customer Roles'>
<CustomerRoleCards
roles={data.quotation.customerRoles}
customers={data.customers}
contacts={data.contacts}
/>
</SectionCard>
<div className='grid gap-4 xl:grid-cols-12'>
<SectionCard title='Item Table' className='xl:col-span-7'>
<div className='space-y-3'>
{data.quotation.items.map((item) => (
<div
key={item.id}
className='grid gap-2 rounded-lg border p-3 md:grid-cols-[1fr_auto_auto]'
>
<div>
<p className='font-medium'>{item.description}</p>
<p className='text-muted-foreground text-sm'>{item.topic}</p>
</div>
<p className='text-sm'>
{item.quantity} {item.unit}
</p>
<p className='text-sm font-semibold'>{formatCurrency(item.amount)}</p>
</div>
))}
</div>
</SectionCard>
<SectionCard title='Total Summary' className='xl:col-span-5'>
<InfoGrid
cols={2}
items={[
{ label: 'Sub Total', value: formatCurrency(data.quotation.totalAmount) },
{ label: 'VAT 7%', value: formatCurrency(data.quotation.totalAmount * 0.07) },
{ label: 'Grand Total', value: formatCurrency(data.quotation.totalAmount * 1.07) },
{ label: 'Hot Project', value: data.quotation.isHotProject ? 'Yes' : 'No' }
]}
/>
</SectionCard>
</div>
<div className='grid gap-4 xl:grid-cols-12'>
<SectionCard title='Topics' className='xl:col-span-5'>
<div className='space-y-4'>
{data.quotation.topics.map((topic) => (
<div key={topic.id}>
<p className='font-medium capitalize'>{topic.label}</p>
<ul className='text-muted-foreground mt-2 space-y-1 text-sm'>
{topic.items.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
))}
</div>
</SectionCard>
<SectionCard title='Attachments' className='xl:col-span-3'>
<div className='space-y-3'>
{data.quotation.attachments.map((attachment) => (
<div key={attachment.id} className='rounded-lg border p-3 text-sm'>
<p className='font-medium'>{attachment.fileName}</p>
<p className='text-muted-foreground'>{attachment.fileType.toUpperCase()}</p>
</div>
))}
</div>
</SectionCard>
<SectionCard title='Follow-ups' className='xl:col-span-4'>
<TimelineList
items={data.quotation.followUps.map((followUp) => ({
id: followUp.id,
title: followUp.title,
detail: `Owner: ${followUp.ownerName}`,
timestamp: followUp.dueDate,
tone: followUp.status === 'done' ? 'success' : 'default'
}))}
/>
</SectionCard>
</div>
<div className='grid gap-4 xl:grid-cols-12'>
<SectionCard title='Approval Timeline' className='xl:col-span-5'>
<TimelineList
items={data.quotation.approvalSteps.map((step) => ({
id: step.id,
title: `Level ${step.level} - ${step.approverName}`,
detail: `${step.approverPosition}${step.comment ? ` / ${step.comment}` : ''}`,
timestamp: step.actedAt ?? data.quotation.quotationDate,
tone:
step.status === 'approved'
? 'success'
: step.status === 'rejected'
? 'danger'
: 'default'
}))}
/>
</SectionCard>
<SectionCard title='Audit Timeline' className='xl:col-span-7'>
<AuditTimeline events={data.auditEvents} />
</SectionCard>
</div>
<QuotationPreviewPanel
quotation={data.quotation}
ownerCustomer={ownerCustomer}
ownerContact={ownerContact}
approvalSteps={data.quotation.approvalSteps}
/>
</div>
);
}