thai-uxui

This commit is contained in:
phaichayon
2026-06-19 11:11:17 +07:00
parent c0cc880be2
commit 721653f692
47 changed files with 1135 additions and 538 deletions

View File

@@ -36,11 +36,11 @@ export function QuotationCellAction({
const deleteMutation = useMutation({
...deleteQuotationMutation,
onSuccess: () => {
toast.success('Quotation deleted successfully');
toast.success('ลบใบเสนอราคาสำเร็จ');
setDeleteOpen(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to delete quotation')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบใบเสนอราคาได้')
});
return (
@@ -65,15 +65,15 @@ export function QuotationCellAction({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem onClick={() => router.push(`/dashboard/crm/quotations/${data.id}`)}>
<Icons.arrowRight className='mr-2 h-4 w-4' /> View
<Icons.arrowRight className='mr-2 h-4 w-4' />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEditOpen(true)} disabled={!canUpdate}>
<Icons.edit className='mr-2 h-4 w-4' /> Edit
<Icons.edit className='mr-2 h-4 w-4' />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteOpen(true)} disabled={!canDelete}>
<Icons.trash className='mr-2 h-4 w-4' /> Delete
<Icons.trash className='mr-2 h-4 w-4' />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -27,7 +27,7 @@ export function getQuotationColumns({
id: 'code',
accessorKey: 'code',
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Quotation' />
<DataTableColumnHeader column={column} title='ใบเสนอราคา' />
),
cell: ({ row }) => (
<div className='flex flex-col'>
@@ -41,8 +41,8 @@ export function getQuotationColumns({
</div>
),
meta: {
label: 'Quotation',
placeholder: 'Search quotations...',
label: 'ใบเสนอราคา',
placeholder: 'ค้นหาใบเสนอราคา...',
variant: 'text' as const,
icon: Icons.search
},
@@ -69,7 +69,7 @@ export function getQuotationColumns({
id: 'quotationType',
accessorKey: 'quotationType',
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Type' />
<DataTableColumnHeader column={column} title='ประเภท' />
),
cell: ({ row }) => (
<Badge variant='outline'>
@@ -77,7 +77,7 @@ export function getQuotationColumns({
</Badge>
),
meta: {
label: 'Type',
label: 'ประเภท',
variant: 'multiSelect' as const,
options: referenceData.quotationTypes.map((item) => ({ value: item.id, label: item.label }))
},
@@ -87,13 +87,13 @@ export function getQuotationColumns({
id: 'branch',
accessorFn: (row) => row.branchId ?? '',
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Branch' />
<DataTableColumnHeader column={column} title='สาขา' />
),
cell: ({ row }) => (
<span>{row.original.branchId ? (branchMap.get(row.original.branchId)?.name ?? 'Unknown') : 'Unassigned'}</span>
),
meta: {
label: 'Branch',
label: 'สาขา',
variant: 'multiSelect' as const,
options: referenceData.branches.map((item) => ({ value: item.id, label: item.name }))
},
@@ -103,11 +103,11 @@ export function getQuotationColumns({
id: 'customer',
accessorFn: (row) => row.customerId,
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Customer' />
<DataTableColumnHeader column={column} title='ลูกค้าผู้รับใบเสนอราคา' />
),
cell: ({ row }) => row.original.customerName,
meta: {
label: 'Customer',
label: 'ลูกค้าผู้รับใบเสนอราคา',
variant: 'multiSelect' as const,
options: referenceData.customers.map((item) => ({ value: item.id, label: item.name }))
},
@@ -117,11 +117,11 @@ export function getQuotationColumns({
id: 'enquiry',
accessorFn: (row) => row.enquiryId ?? '',
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Enquiry' />
<DataTableColumnHeader column={column} title='โอกาสขาย' />
),
cell: ({ row }) => row.original.enquiryCode ?? '-',
meta: {
label: 'Enquiry',
label: 'โอกาสขาย',
variant: 'multiSelect' as const,
options: referenceData.enquiries.map((item) => ({ value: item.id, label: `${item.code} - ${item.title}` }))
},
@@ -131,15 +131,15 @@ export function getQuotationColumns({
id: 'isHotProject',
accessorFn: (row) => String(row.isHotProject),
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Hot' />
<DataTableColumnHeader column={column} title='ด่วน' />
),
cell: ({ row }) => (row.original.isHotProject ? <Badge>Hot</Badge> : <span>-</span>),
meta: {
label: 'Hot Project',
label: 'โครงการด่วน',
variant: 'multiSelect' as const,
options: [
{ value: 'true', label: 'Hot' },
{ value: 'false', label: 'Normal' }
{ value: 'true', label: 'ด่วน' },
{ value: 'false', label: 'ปกติ' }
]
},
enableColumnFilter: true
@@ -148,7 +148,7 @@ export function getQuotationColumns({
id: 'totalAmount',
accessorKey: 'totalAmount',
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Total' />
<DataTableColumnHeader column={column} title='มูลค่ารวม' />
),
cell: ({ row }) => row.original.totalAmount.toLocaleString()
},

View File

@@ -1324,7 +1324,7 @@ export function QuotationDetail({
<TabsList className="flex flex-wrap">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="items">Items</TabsTrigger>
<TabsTrigger value="customers">Project Parties</TabsTrigger>
<TabsTrigger value="customers"></TabsTrigger>
<TabsTrigger value="topics">Topics</TabsTrigger>
<TabsTrigger value="followups">Follow-ups</TabsTrigger>
<TabsTrigger value="attachments">Attachments</TabsTrigger>
@@ -1343,7 +1343,7 @@ export function QuotationDetail({
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<FieldItem
label="Billing Customer"
label="ลูกค้าผู้รับใบเสนอราคา"
value={customer?.name}
/>
<FieldItem label="Contact" value={contact?.name} />
@@ -1406,7 +1406,7 @@ export function QuotationDetail({
<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">
@@ -1534,7 +1534,7 @@ export function QuotationDetail({
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Project Parties</CardTitle>
<CardTitle></CardTitle>
<CardDescription>
Related companies for this quotation, each with a
visible role.

View File

@@ -19,9 +19,9 @@ 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' }
{ key: 'scope', title: 'ขอบเขตงาน' },
{ key: 'exclusion', title: 'ข้อยกเว้น' },
{ key: 'payment', title: 'เงื่อนไขการชำระเงิน' }
];
return (
@@ -32,14 +32,14 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<div className='space-y-1'>
<CardTitle className='text-2xl'>{documentData.company.name}</CardTitle>
<CardDescription>
Quotation Preview using {template.template.templateName} v{template.version.version}
{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></Badge>
)}
<Badge variant='outline'>{template.template.fileType}</Badge>
</div>
@@ -49,41 +49,41 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<div className='grid gap-6 lg:grid-cols-2'>
<Card>
<CardHeader>
<CardTitle>Quotation Header</CardTitle>
<CardDescription>Commercial identity and issue information.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2'>
<Field label='Quotation Code' value={documentData.quotation.code} />
<Field label='เลขที่ใบเสนอราคา' value={documentData.quotation.code} />
<Field label='Revision' value={documentData.quotation.revisionLabel} />
<Field
label='Quotation Date'
label='วันที่ออกใบเสนอราคา'
value={new Date(documentData.quotation.quotationDate).toLocaleDateString()}
/>
<Field
label='Valid Until'
label='ใช้ได้ถึง'
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} />
<Field label='ประเภทเอกสาร' value={documentData.quotation.quotationTypeLabel} />
<Field label='สาขา' value={documentData.branch?.label} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Customer Block</CardTitle>
<CardDescription>Bill-to and attention context for the document.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></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} />
<Field label='ลูกค้าผู้รับใบเสนอราคา' value={documentData.customer.name} />
<Field label='ผู้ติดต่อ' value={documentData.contact?.name} />
<Field label='โทรศัพท์' value={documentData.customer.phone} />
<Field label='อีเมล' value={documentData.customer.email} />
<div className='md:col-span-2'>
<Field label='Address' value={documentData.customer.address} />
<Field label='ที่อยู่' value={documentData.customer.address} />
</div>
</CardContent>
</Card>
@@ -91,30 +91,30 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<Card>
<CardHeader>
<CardTitle>Project Information</CardTitle>
<CardDescription>Scope summary and reference data for the customer.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></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} />
<Field label='ชื่อโครงการ' value={documentData.quotation.projectName} />
<Field label='สถานที่ติดตั้ง' value={documentData.quotation.projectLocation} />
<Field label='เรียน' value={documentData.quotation.attention} />
<Field label='อ้างอิง' value={documentData.quotation.reference} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Items Table</CardTitle>
<CardDescription>Normalized line items prepared for template mapping.</CardDescription>
<CardTitle></CardTitle>
<CardDescription> 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></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
{documentData.items.map((item) => (
<div
@@ -138,8 +138,8 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<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>
<CardTitle></CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className='space-y-6'>
{topicGroups.map((group) => {
@@ -148,7 +148,7 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<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>
<div className='text-muted-foreground text-sm'></div>
) : (
entries.map((entry) => (
<div key={`${group.key}-${entry.title}`} className='rounded-lg border p-4'>
@@ -170,19 +170,19 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<div className='space-y-6'>
<Card>
<CardHeader>
<CardTitle>Price Summary</CardTitle>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className='space-y-3 text-sm'>
<div className='flex items-center justify-between'>
<span className='text-muted-foreground'>Subtotal</span>
<span className='text-muted-foreground'></span>
<span>{documentData.quotation.subtotal.toLocaleString()}</span>
</div>
<div className='flex items-center justify-between'>
<span className='text-muted-foreground'>Tax</span>
<span className='text-muted-foreground'></span>
<span>{documentData.quotation.taxAmount.toLocaleString()}</span>
</div>
<div className='flex items-center justify-between'>
<span className='text-muted-foreground'>Total</span>
<span className='text-muted-foreground'></span>
<span className='font-semibold'>{documentData.quotation.totalAmount.toLocaleString()}</span>
</div>
</CardContent>
@@ -190,24 +190,24 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<Card>
<CardHeader>
<CardTitle>Approval Block</CardTitle>
<CardDescription>Prepared for watermark and approval snapshot flows.</CardDescription>
<CardTitle></CardTitle>
<CardDescription> watermark approved snapshot</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<Field label='Status' value={documentData.approval.status} />
<Field label='สถานะ' value={documentData.approval.status} />
<Field label='Workflow' value={documentData.approval.workflowName} />
<Field label='Approved At' value={documentData.approval.approvedAt} />
<Field label='อนุมัติเมื่อ' value={documentData.approval.approvedAt} />
<Separator />
{!documentData.approval.approvers.length ? (
<div className='text-muted-foreground text-sm'>No approval actions recorded.</div>
<div className='text-muted-foreground text-sm'></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}
{item.stepNumber} - {item.roleName}
</div>
<div className='text-muted-foreground'>
{item.actorName || '-'} {item.actedAt ? `on ${new Date(item.actedAt).toLocaleString()}` : ''}
{item.actorName || '-'} {item.actedAt ? `เมื่อ ${new Date(item.actedAt).toLocaleString()}` : ''}
</div>
</div>
))
@@ -217,17 +217,17 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
<Card>
<CardHeader>
<CardTitle>Signature Placeholders</CardTitle>
<CardTitle></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='ผู้จัดทำ' value={documentData.signatures.preparedBy} />
<Field label='ผู้ขออนุมัติ' value={documentData.signatures.requestedBy} />
<Field label='ผู้จัดการฝ่ายขาย' value={documentData.signatures.salesManager} />
<Field
label='Department Manager'
label='ผู้จัดการฝ่าย'
value={documentData.signatures.departmentManager}
/>
<Field label='Top Manager' value={documentData.signatures.topManager} />
<Field label='ผู้บริหารสูงสุด' value={documentData.signatures.topManager} />
</CardContent>
</Card>
</div>

View File

@@ -268,7 +268,7 @@ export function QuotationFormSheet({
</Select>
</Field>
<Field label='Billing Customer'>
<Field label='ลูกค้าผู้รับใบเสนอราคา'>
<Select
value={state.customerId}
onValueChange={(value) => {