240 lines
11 KiB
TypeScript
240 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { quotationDocumentPreviewOptions } from '@/features/crm/quotations/document/queries';
|
|
|
|
function Field({ label, value }: { label: string; value: string | null | undefined }) {
|
|
return (
|
|
<div className='space-y-1'>
|
|
<div className='text-muted-foreground text-xs'>{label}</div>
|
|
<div className='text-sm'>{value || '-'}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function QuotationDocumentPreview({ quotationId }: { quotationId: string }) {
|
|
const { data } = useSuspenseQuery(quotationDocumentPreviewOptions(quotationId));
|
|
const { documentData, template } = data.preview;
|
|
const topicGroups: Array<{ key: 'scope' | 'exclusion' | 'payment'; title: string }> = [
|
|
{ key: 'scope', title: 'Scope' },
|
|
{ key: 'exclusion', title: 'Exclusions' },
|
|
{ key: 'payment', title: 'Payment Terms' }
|
|
];
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<Card className='overflow-hidden'>
|
|
<CardHeader className='border-b bg-muted/20'>
|
|
<div className='flex flex-wrap items-start justify-between gap-4'>
|
|
<div className='space-y-1'>
|
|
<CardTitle className='text-2xl'>{documentData.company.name}</CardTitle>
|
|
<CardDescription>
|
|
Quotation Preview using {template.template.templateName} v{template.version.version}
|
|
</CardDescription>
|
|
</div>
|
|
<div className='flex flex-wrap gap-2'>
|
|
{documentData.watermarkStatus ? (
|
|
<Badge variant='destructive'>{documentData.watermarkStatus}</Badge>
|
|
) : (
|
|
<Badge>Approved Ready</Badge>
|
|
)}
|
|
<Badge variant='outline'>{template.template.fileType}</Badge>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className='space-y-8 p-6'>
|
|
<div className='grid gap-6 lg:grid-cols-2'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Quotation Header</CardTitle>
|
|
<CardDescription>Commercial identity and issue information.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='grid gap-4 md:grid-cols-2'>
|
|
<Field label='Quotation Code' value={documentData.quotation.code} />
|
|
<Field label='Revision' value={documentData.quotation.revisionLabel} />
|
|
<Field
|
|
label='Quotation Date'
|
|
value={new Date(documentData.quotation.quotationDate).toLocaleDateString()}
|
|
/>
|
|
<Field
|
|
label='Valid Until'
|
|
value={
|
|
documentData.quotation.validUntil
|
|
? new Date(documentData.quotation.validUntil).toLocaleDateString()
|
|
: null
|
|
}
|
|
/>
|
|
<Field label='Type' value={documentData.quotation.quotationTypeLabel} />
|
|
<Field label='Branch' value={documentData.branch?.label} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Customer Block</CardTitle>
|
|
<CardDescription>Bill-to and attention context for the document.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='grid gap-4 md:grid-cols-2'>
|
|
<Field label='Billing Customer' value={documentData.customer.name} />
|
|
<Field label='Contact' value={documentData.contact?.name} />
|
|
<Field label='Phone' value={documentData.customer.phone} />
|
|
<Field label='Email' value={documentData.customer.email} />
|
|
<div className='md:col-span-2'>
|
|
<Field label='Address' value={documentData.customer.address} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Project Information</CardTitle>
|
|
<CardDescription>Scope summary and reference data for the customer.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
|
<Field label='Project Name' value={documentData.quotation.projectName} />
|
|
<Field label='Site Location' value={documentData.quotation.projectLocation} />
|
|
<Field label='Attention' value={documentData.quotation.attention} />
|
|
<Field label='Reference' value={documentData.quotation.reference} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Items Table</CardTitle>
|
|
<CardDescription>Normalized line items prepared for template mapping.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='space-y-3'>
|
|
<div className='grid grid-cols-[1.2fr_4fr_1fr_1.2fr_1.4fr_1.4fr] gap-3 rounded-lg border bg-muted/20 px-4 py-3 text-xs font-medium uppercase tracking-wide'>
|
|
<div>Item</div>
|
|
<div>Description</div>
|
|
<div>Qty</div>
|
|
<div>Unit</div>
|
|
<div>Unit Price</div>
|
|
<div>Total</div>
|
|
</div>
|
|
{documentData.items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className='grid grid-cols-[1.2fr_4fr_1fr_1.2fr_1.4fr_1.4fr] gap-3 rounded-lg border px-4 py-3 text-sm'
|
|
>
|
|
<div>{item.itemNumber}</div>
|
|
<div className='space-y-1'>
|
|
<div>{item.description}</div>
|
|
<div className='text-muted-foreground text-xs'>{item.productTypeLabel || '-'}</div>
|
|
</div>
|
|
<div>{item.quantity}</div>
|
|
<div>{item.unitLabel || '-'}</div>
|
|
<div>{item.unitPrice.toLocaleString()}</div>
|
|
<div>{item.totalPrice.toLocaleString()}</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className='grid gap-6 lg:grid-cols-[2fr_1fr]'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Topics</CardTitle>
|
|
<CardDescription>Scope, exclusions, and payment terms from topic records.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='space-y-6'>
|
|
{topicGroups.map((group) => {
|
|
const entries = documentData.topics[group.key];
|
|
return (
|
|
<div key={group.key} className='space-y-3'>
|
|
<div className='font-medium'>{group.title}</div>
|
|
{!entries.length ? (
|
|
<div className='text-muted-foreground text-sm'>No entries</div>
|
|
) : (
|
|
entries.map((entry) => (
|
|
<div key={`${group.key}-${entry.title}`} className='rounded-lg border p-4'>
|
|
<div className='mb-2 text-sm font-medium'>{entry.title}</div>
|
|
<ul className='list-disc space-y-1 pl-5 text-sm'>
|
|
{entry.items.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className='space-y-6'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Price Summary</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='space-y-3 text-sm'>
|
|
<div className='flex items-center justify-between'>
|
|
<span className='text-muted-foreground'>Subtotal</span>
|
|
<span>{documentData.quotation.subtotal.toLocaleString()}</span>
|
|
</div>
|
|
<div className='flex items-center justify-between'>
|
|
<span className='text-muted-foreground'>Tax</span>
|
|
<span>{documentData.quotation.taxAmount.toLocaleString()}</span>
|
|
</div>
|
|
<div className='flex items-center justify-between'>
|
|
<span className='text-muted-foreground'>Total</span>
|
|
<span className='font-semibold'>{documentData.quotation.totalAmount.toLocaleString()}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Approval Block</CardTitle>
|
|
<CardDescription>Prepared for watermark and approval snapshot flows.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='space-y-4'>
|
|
<Field label='Status' value={documentData.approval.status} />
|
|
<Field label='Workflow' value={documentData.approval.workflowName} />
|
|
<Field label='Approved At' value={documentData.approval.approvedAt} />
|
|
<Separator />
|
|
{!documentData.approval.approvers.length ? (
|
|
<div className='text-muted-foreground text-sm'>No approval actions recorded.</div>
|
|
) : (
|
|
documentData.approval.approvers.map((item) => (
|
|
<div key={`${item.stepNumber}-${item.roleCode}`} className='rounded-lg border p-3 text-sm'>
|
|
<div className='font-medium'>
|
|
Step {item.stepNumber} - {item.roleName}
|
|
</div>
|
|
<div className='text-muted-foreground'>
|
|
{item.actorName || '-'} {item.actedAt ? `on ${new Date(item.actedAt).toLocaleString()}` : ''}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Signature Placeholders</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='space-y-3 text-sm'>
|
|
<Field label='Prepared By' value={documentData.signatures.preparedBy} />
|
|
<Field label='Requested By' value={documentData.signatures.requestedBy} />
|
|
<Field label='Sales Manager' value={documentData.signatures.salesManager} />
|
|
<Field
|
|
label='Department Manager'
|
|
value={documentData.signatures.departmentManager}
|
|
/>
|
|
<Field label='Top Manager' value={documentData.signatures.topManager} />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|