task-g
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
'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;
|
||||
|
||||
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='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'>
|
||||
{[
|
||||
{ key: 'scope', title: 'Scope' },
|
||||
{ key: 'exclusion', title: 'Exclusions' },
|
||||
{ key: 'payment', title: 'Payment Terms' }
|
||||
].map((group) => {
|
||||
const entries = documentData.topics[group.key as keyof typeof documentData.topics];
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user