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

@@ -75,19 +75,19 @@ export function ProjectPartiesEditor({
<div className='space-y-3'>
<div className='flex items-center justify-between gap-3'>
<div>
<div className='text-sm font-medium'>Project Parties</div>
<div className='text-sm font-medium'></div>
<p className='text-muted-foreground text-sm'>
Add related companies with their role and optional remark.
</p>
</div>
<Button type='button' variant='outline' size='sm' onClick={addItem}>
<Icons.add className='mr-2 h-4 w-4' /> Add Party
<Icons.add className='mr-2 h-4 w-4' />
</Button>
</div>
{!items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
No project parties yet. Billing customer will still stay separate from this list.
</div>
) : null}
@@ -95,14 +95,14 @@ export function ProjectPartiesEditor({
{items.map((item, index) => (
<div key={item.key} className='rounded-lg border p-4'>
<div className='mb-3 flex items-center justify-between gap-3'>
<div className='text-sm font-medium'>Party {index + 1}</div>
<div className='text-sm font-medium'> {index + 1}</div>
<Button
type='button'
variant='ghost'
size='sm'
onClick={() => removeItem(item.key)}
>
<Icons.trash className='mr-2 h-4 w-4' /> Remove
<Icons.trash className='mr-2 h-4 w-4' />
</Button>
</div>
<div className='grid gap-3 md:grid-cols-3'>
@@ -113,10 +113,10 @@ export function ProjectPartiesEditor({
}
>
<SelectTrigger>
<SelectValue placeholder='Customer' />
<SelectValue placeholder='ลูกค้า' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__none__'>Select customer</SelectItem>
<SelectItem value='__none__'></SelectItem>
{customers.map((customer) => (
<SelectItem key={customer.id} value={customer.id}>
{customer.name} ({customer.code})
@@ -127,10 +127,10 @@ export function ProjectPartiesEditor({
<Select value={item.role || '__none__'} onValueChange={(value) => updateItem(item.key, 'role', value === '__none__' ? '' : value)}>
<SelectTrigger>
<SelectValue placeholder='Role' />
<SelectValue placeholder='บทบาท' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__none__'>Select role</SelectItem>
<SelectItem value='__none__'></SelectItem>
{roles.map((role) => (
<SelectItem key={role.id} value={role.id}>
{role.label}
@@ -142,7 +142,7 @@ export function ProjectPartiesEditor({
<Input
value={item.remark}
onChange={(event) => updateItem(item.key, 'remark', event.target.value)}
placeholder='Remark'
placeholder='หมายเหตุ'
/>
</div>
</div>

View File

@@ -51,21 +51,21 @@ export function ContactFormSheet({
const createMutation = useMutation({
...createCustomerContactMutation,
onSuccess: () => {
toast.success('Contact created successfully');
toast.success('สร้างผู้ติดต่อสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to create contact')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถสร้างผู้ติดต่อได้')
});
const updateMutation = useMutation({
...updateCustomerContactMutation,
onSuccess: () => {
toast.success('Contact updated successfully');
toast.success('อัปเดตผู้ติดต่อสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to update contact')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอัปเดตผู้ติดต่อได้')
});
const form = useAppForm({
@@ -106,43 +106,41 @@ export function ContactFormSheet({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className='flex flex-col sm:max-w-2xl'>
<SheetHeader>
<SheetTitle>{isEdit ? 'Edit Contact' : 'New Contact'}</SheetTitle>
<SheetDescription>
Capture the operational contact person for this customer account.
</SheetDescription>
<SheetTitle>{isEdit ? 'แก้ไขผู้ติดต่อ' : 'เพิ่มผู้ติดต่อ'}</SheetTitle>
<SheetDescription></SheetDescription>
</SheetHeader>
<div className='flex-1 overflow-auto'>
<form.AppForm>
<form.Form id='customer-contact-form' className='grid gap-4 md:grid-cols-2'>
<FormTextField name='name' label='Contact Name' required placeholder='Jane Doe' />
<FormTextField name='position' label='Position' placeholder='Procurement Manager' />
<FormTextField name='department' label='Department' placeholder='Procurement' />
<FormTextField name='phone' label='Phone' placeholder='02-000-0000' />
<FormTextField name='mobile' label='Mobile' placeholder='081-000-0000' />
<FormTextField name='name' label='ชื่อผู้ติดต่อ' required placeholder='Jane Doe' />
<FormTextField name='position' label='ตำแหน่ง' placeholder='Procurement Manager' />
<FormTextField name='department' label='แผนก' placeholder='Procurement' />
<FormTextField name='phone' label='โทรศัพท์' placeholder='02-000-0000' />
<FormTextField name='mobile' label='มือถือ' placeholder='081-000-0000' />
<FormTextField
name='email'
label='Email'
label='อีเมล'
type='email'
placeholder='jane@example.com'
/>
<div className='md:col-span-2'>
<FormTextareaField
name='notes'
label='Notes'
placeholder='Preferred communication style, escalation notes, or meeting context'
label='หมายเหตุ'
placeholder='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม'
/>
</div>
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
<FormSwitchField
name='isPrimary'
label='Primary Contact'
description='When enabled, this contact becomes the main person for this customer.'
label='ผู้ติดต่อหลัก'
description='เมื่อเปิดใช้งาน ผู้ติดต่อนี้จะเป็นผู้ติดต่อหลักของลูกค้ารายนี้'
/>
<FormSwitchField
name='isActive'
label='Active Contact'
description='Inactive contacts remain in history and can be restored later.'
label='ใช้งานอยู่'
description='ผู้ติดต่อที่ปิดใช้งานยังคงอยู่ในประวัติและเปิดกลับมาใช้ได้ภายหลัง'
/>
</div>
</form.Form>
@@ -151,11 +149,11 @@ export function ContactFormSheet({
<SheetFooter>
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type='submit' form='customer-contact-form' isLoading={isPending}>
<Icons.check className='mr-2 h-4 w-4' />
{isEdit ? 'Update Contact' : 'Create Contact'}
{isEdit ? 'อัปเดตผู้ติดต่อ' : 'สร้างผู้ติดต่อ'}
</Button>
</SheetFooter>
</SheetContent>

View File

@@ -36,11 +36,11 @@ export function CustomerCellAction({
const deleteMutation = useMutation({
...deleteCustomerMutation,
onSuccess: () => {
toast.success('Customer deleted successfully');
toast.success('ลบลูกค้าสำเร็จ');
setDeleteOpen(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to delete customer')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบลูกค้าได้')
});
return (
@@ -65,15 +65,15 @@ export function CustomerCellAction({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem onClick={() => router.push(`/dashboard/crm/customers/${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

@@ -29,7 +29,7 @@ export function getCustomerColumns({
id: 'name',
accessorKey: 'name',
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Customer' />
<DataTableColumnHeader column={column} title='ลูกค้า' />
),
cell: ({ row }) => (
<div className='flex flex-col'>
@@ -46,8 +46,8 @@ export function getCustomerColumns({
</div>
),
meta: {
label: 'Customer',
placeholder: 'Search customers...',
label: 'ลูกค้า',
placeholder: 'ค้นหาลูกค้า...',
variant: 'text' as const,
icon: Icons.search
},
@@ -85,7 +85,7 @@ export function getCustomerColumns({
</Badge>
),
meta: {
label: 'Type',
label: 'ประเภท',
variant: 'multiSelect' as const,
options: referenceData.customerTypes.map((item) => ({
value: item.id,
@@ -108,7 +108,7 @@ export function getCustomerColumns({
</span>
),
meta: {
label: 'Branch',
label: 'สาขา',
variant: 'multiSelect' as const,
options: referenceData.branches.map((item) => ({
value: item.id,
@@ -137,7 +137,7 @@ export function getCustomerColumns({
id: 'contactCount',
accessorKey: 'contactCount',
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Contacts' />
<DataTableColumnHeader column={column} title='ผู้ติดต่อ' />
),
cell: ({ row }) => <span>{row.original.contactCount}</span>
},

View File

@@ -32,20 +32,20 @@ export function CustomerContactsTab({
const deleteMutation = useMutation({
...deleteCustomerContactMutation,
onSuccess: () => {
toast.success('Contact deleted successfully');
toast.success('ลบผู้ติดต่อสำเร็จ');
setDeleteOpen(false);
setSelectedId(null);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to delete contact')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบผู้ติดต่อได้')
});
return (
<Card>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div>
<CardTitle>Contacts</CardTitle>
<CardDescription>Manage the people attached to this customer account.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
{canCreate ? (
<Button
@@ -55,14 +55,14 @@ export function CustomerContactsTab({
setOpen(true);
}}
>
<Icons.add className='mr-2 h-4 w-4' /> Add Contact
<Icons.add className='mr-2 h-4 w-4' />
</Button>
) : null}
</CardHeader>
<CardContent className='space-y-4'>
{!data.items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No contacts have been added yet.
</div>
) : (
data.items.map((contact) => (
@@ -73,16 +73,16 @@ export function CustomerContactsTab({
<div className='space-y-1'>
<div className='flex flex-wrap items-center gap-2'>
<span className='font-medium'>{contact.name}</span>
{contact.isPrimary ? <Badge>Primary</Badge> : null}
{!contact.isActive ? <Badge variant='outline'>Inactive</Badge> : null}
{contact.isPrimary ? <Badge></Badge> : null}
{!contact.isActive ? <Badge variant='outline'></Badge> : null}
</div>
<div className='text-muted-foreground text-sm'>
{[contact.position, contact.department].filter(Boolean).join(' • ') ||
'No role details'}
'ยังไม่มีข้อมูลตำแหน่ง'}
</div>
<div className='text-muted-foreground text-sm'>
{[contact.phone, contact.mobile, contact.email].filter(Boolean).join(' • ') ||
'No contact methods'}
'ยังไม่มีช่องทางติดต่อ'}
</div>
{contact.notes ? <p className='text-sm'>{contact.notes}</p> : null}
</div>
@@ -96,7 +96,7 @@ export function CustomerContactsTab({
setOpen(true);
}}
>
<Icons.edit className='mr-2 h-4 w-4' /> Edit
<Icons.edit className='mr-2 h-4 w-4' />
</Button>
<Button
variant='outline'
@@ -107,7 +107,7 @@ export function CustomerContactsTab({
setDeleteOpen(true);
}}
>
<Icons.trash className='mr-2 h-4 w-4' /> Delete
<Icons.trash className='mr-2 h-4 w-4' />
</Button>
</div>
</div>

View File

@@ -100,14 +100,14 @@ export function CustomerDetail({
<div>
<h2 className='text-2xl font-semibold'>{customer.name}</h2>
<p className='text-muted-foreground text-sm'>
{customer.abbr || customer.email || customer.phone || 'Customer detail record'}
{customer.abbr || customer.email || customer.phone || 'รายละเอียดข้อมูลลูกค้า'}
</p>
</div>
</div>
<div className='flex flex-wrap gap-2'>
{canUpdate ? (
<Button variant='outline' onClick={() => setEditOpen(true)}>
<Icons.edit className='mr-2 h-4 w-4' /> Edit Customer
<Icons.edit className='mr-2 h-4 w-4' />
</Button>
) : null}
</div>
@@ -119,18 +119,16 @@ export function CustomerDetail({
<CardContent className='pt-6'>
<Tabs defaultValue='overview' className='gap-4'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
{canReadContacts ? <TabsTrigger value='contacts'>Contacts</TabsTrigger> : null}
<TabsTrigger value='activity'>Activity</TabsTrigger>
<TabsTrigger value='related'>Related Documents</TabsTrigger>
<TabsTrigger value='overview'></TabsTrigger>
{canReadContacts ? <TabsTrigger value='contacts'></TabsTrigger> : null}
<TabsTrigger value='activity'></TabsTrigger>
<TabsTrigger value='related'></TabsTrigger>
</TabsList>
<TabsContent value='overview'>
<Card>
<CardHeader>
<CardTitle>Overview</CardTitle>
<CardDescription>
Core account data, location, and CRM classification.
</CardDescription>
<CardTitle></CardTitle>
<CardDescription> CRM</CardDescription>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<FieldItem label='Tax ID' value={customer.taxId} />
@@ -142,21 +140,21 @@ export function CustomerDetail({
value={customer.branchId ? branchMap.get(customer.branchId) : null}
/>
<FieldItem
label='Lead Channel'
label='ที่มาของลีด'
value={leadMap.get(customer.leadChannel ?? '')}
/>
<FieldItem
label='Customer Group'
label='กลุ่มลูกค้า'
value={groupMap.get(customer.customerGroup ?? '')}
/>
<FieldItem
label='Customer Sub Group'
label='กลุ่มย่อยลูกค้า'
value={subGroupMap.get(customer.customerSubGroup ?? '')}
/>
<FieldItem label='Active' value={customer.isActive ? 'Yes' : 'No'} />
<FieldItem label='ใช้งานอยู่' value={customer.isActive ? 'ใช่' : 'ไม่ใช่'} />
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem
label='Address'
label='ที่อยู่'
value={
[
customer.address,
@@ -172,7 +170,7 @@ export function CustomerDetail({
/>
</div>
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem label='Notes' value={customer.notes} />
<FieldItem label='หมายเหตุ' value={customer.notes} />
</div>
</CardContent>
</Card>
@@ -190,15 +188,13 @@ export function CustomerDetail({
<TabsContent value='activity'>
<Card>
<CardHeader>
<CardTitle>Activity</CardTitle>
<CardDescription>
Audit log entries captured from customer mutations.
</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{!data.activity.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No activity recorded yet.
</div>
) : (
data.activity.map((item, index) => (
@@ -229,15 +225,13 @@ export function CustomerDetail({
<TabsContent value='related'>
<Card>
<CardHeader>
<CardTitle>Related Documents</CardTitle>
<CardDescription>
Production enquiry and quotation links for this customer.
</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{!relatedEnquiries.length && !relatedQuotations.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No enquiries or quotations have been linked to this customer yet.
</div>
) : (
<div className='space-y-3'>
@@ -288,8 +282,8 @@ export function CustomerDetail({
<div className='space-y-6'>
<Card>
<CardHeader>
<CardTitle>Record Snapshot</CardTitle>
<CardDescription>Operational metadata for this customer row.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<FieldItem label='Created By' value={customer.createdBy} />

View File

@@ -73,21 +73,21 @@ export function CustomerFormSheet({
const createMutation = useMutation({
...createCustomerMutation,
onSuccess: () => {
toast.success('Customer created successfully');
toast.success('สร้างลูกค้าสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to create customer')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถสร้างลูกค้าได้')
});
const updateMutation = useMutation({
...updateCustomerMutation,
onSuccess: () => {
toast.success('Customer updated successfully');
toast.success('อัปเดตลูกค้าสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to update customer')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอัปเดตลูกค้าได้')
});
const form = useAppForm({
@@ -146,10 +146,8 @@ export function CustomerFormSheet({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className='flex flex-col sm:max-w-3xl'>
<SheetHeader>
<SheetTitle>{isEdit ? 'Edit Customer' : 'New Customer'}</SheetTitle>
<SheetDescription>
Maintain the core account profile, ownership branch, and CRM classification here.
</SheetDescription>
<SheetTitle>{isEdit ? 'แก้ไขลูกค้า' : 'เพิ่มลูกค้า'}</SheetTitle>
<SheetDescription> CRM</SheetDescription>
</SheetHeader>
<div className='flex-1 overflow-auto'>
@@ -157,25 +155,25 @@ export function CustomerFormSheet({
<form.Form id='customer-form-sheet' className='grid gap-4 md:grid-cols-2'>
<FormTextField
name='name'
label='Customer Name'
label='ชื่อลูกค้า'
required
placeholder='ALLA Service Co., Ltd.'
/>
<FormTextField name='abbr' label='Abbreviation' placeholder='ALLA' />
<FormTextField name='taxId' label='Tax ID' placeholder='0105556...' />
<FormTextField name='phone' label='Phone' placeholder='02-000-0000' />
<FormTextField name='fax' label='Fax' placeholder='02-000-0001' />
<FormTextField name='abbr' label='ชื่อย่อ' placeholder='ALLA' />
<FormTextField name='taxId' label='เลขประจำตัวผู้เสียภาษี' placeholder='0105556...' />
<FormTextField name='phone' label='โทรศัพท์' placeholder='02-000-0000' />
<FormTextField name='fax' label='แฟกซ์' placeholder='02-000-0001' />
<FormTextField
name='email'
label='Email'
label='อีเมล'
type='email'
placeholder='sales@example.com'
/>
<FormTextField name='website' label='Website' placeholder='https://example.com' />
<FormTextField name='country' label='Country' placeholder='Thailand' />
<FormTextField name='website' label='เว็บไซต์' placeholder='https://example.com' />
<FormTextField name='country' label='ประเทศ' placeholder='Thailand' />
<FormSelectField
name='customerType'
label='Customer Type'
label='ประเภทลูกค้า'
required
options={referenceData.customerTypes.map((item) => ({
value: item.id,
@@ -184,7 +182,7 @@ export function CustomerFormSheet({
/>
<FormSelectField
name='customerStatus'
label='Customer Status'
label='สถานะลูกค้า'
required
options={referenceData.customerStatuses.map((item) => ({
value: item.id,
@@ -193,7 +191,7 @@ export function CustomerFormSheet({
/>
<FormSelectField
name='branchId'
label='Branch'
label='สาขา'
options={referenceData.branches.map((branch) => ({
value: branch.id,
label: branch.name
@@ -201,7 +199,7 @@ export function CustomerFormSheet({
/>
<FormSelectField
name='leadChannel'
label='Lead Channel'
label='ที่มาของลีด'
options={referenceData.leadChannels.map((item) => ({
value: item.id,
label: item.label
@@ -209,7 +207,7 @@ export function CustomerFormSheet({
/>
<FormSelectField
name='customerGroup'
label='Customer Group'
label='กลุ่มลูกค้า'
options={referenceData.customerGroups.map((item) => ({
value: item.id,
label: item.label
@@ -217,10 +215,10 @@ export function CustomerFormSheet({
/>
<FormSelectField
name='customerSubGroup'
label='Customer Sub Group'
label='กลุ่มย่อยลูกค้า'
disabled={!selectedCustomerGroup}
placeholder={
selectedCustomerGroup ? 'Select customer sub group' : 'Select customer group first'
selectedCustomerGroup ? 'เลือกกลุ่มย่อยลูกค้า' : 'เลือกกลุ่มลูกค้าก่อน'
}
options={filteredCustomerSubGroups.map((item) => ({
value: item.id,
@@ -228,24 +226,24 @@ export function CustomerFormSheet({
}))}
/>
<div className='md:col-span-2'>
<FormTextField name='address' label='Address' placeholder='123 Main Road' />
<FormTextField name='address' label='ที่อยู่' placeholder='123 Main Road' />
</div>
<FormTextField name='province' label='Province' placeholder='Bangkok' />
<FormTextField name='district' label='District' placeholder='Bang Kapi' />
<FormTextField name='subDistrict' label='Sub District' placeholder='Hua Mak' />
<FormTextField name='postalCode' label='Postal Code' placeholder='10240' />
<FormTextField name='province' label='จังหวัด' placeholder='Bangkok' />
<FormTextField name='district' label='เขต / อำเภอ' placeholder='Bang Kapi' />
<FormTextField name='subDistrict' label='แขวง / ตำบล' placeholder='Hua Mak' />
<FormTextField name='postalCode' label='รหัสไปรษณีย์' placeholder='10240' />
<div className='md:col-span-2'>
<FormTextareaField
name='notes'
label='Notes'
placeholder='Internal notes about this customer relationship'
label='หมายเหตุ'
placeholder='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้'
/>
</div>
<div className='md:col-span-2'>
<FormSwitchField
name='isActive'
label='Active Record'
description='Inactive customers remain visible in history but should not be used for new work.'
label='ใช้งานอยู่'
description='ลูกค้าที่ปิดใช้งานยังคงอยู่ในประวัติ แต่ไม่ควรนำไปใช้กับงานใหม่'
/>
</div>
</form.Form>
@@ -254,11 +252,11 @@ export function CustomerFormSheet({
<SheetFooter>
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type='submit' form='customer-form-sheet' isLoading={isPending}>
<Icons.check className='mr-2 h-4 w-4' />
{isEdit ? 'Update Customer' : 'Create Customer'}
{isEdit ? 'อัปเดตลูกค้า' : 'สร้างลูกค้า'}
</Button>
</SheetFooter>
</SheetContent>
@@ -276,7 +274,7 @@ export function CustomerFormSheetTrigger({
return (
<>
<Button onClick={() => setOpen(true)}>
<Icons.add className='mr-2 h-4 w-4' /> Add Customer
<Icons.add className='mr-2 h-4 w-4' />
</Button>
<CustomerFormSheet open={open} onOpenChange={setOpen} referenceData={referenceData} />
</>

View File

@@ -24,42 +24,42 @@ export function DashboardApprovalMetrics({
return (
<Card>
<CardHeader>
<CardTitle>Approval Analytics</CardTitle>
<CardDescription>Live workflow counts plus the current approver queue.</CardDescription>
<CardTitle></CardTitle>
<CardDescription> workflow </CardDescription>
</CardHeader>
<CardContent className='space-y-6'>
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-5'>
<Stat label='Pending Approvals' value={approvals.summary.pendingApprovals} />
<Stat label='Approved Today' value={approvals.summary.approvedToday} />
<Stat label='Rejected Today' value={approvals.summary.rejectedToday} />
<Stat label='Returned Today' value={approvals.summary.returnedToday} />
<Stat label='รออนุมัติ' value={approvals.summary.pendingApprovals} />
<Stat label='อนุมัติวันนี้' value={approvals.summary.approvedToday} />
<Stat label='ไม่อนุมัติวันนี้' value={approvals.summary.rejectedToday} />
<Stat label='ตีกลับวันนี้' value={approvals.summary.returnedToday} />
<Stat
label='Average Approval Time'
label='เวลาอนุมัติเฉลี่ย'
value={approvals.summary.averageApprovalTimeHours}
suffix='hrs'
suffix='ชม.'
/>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Document</TableHead>
<TableHead></TableHead>
<TableHead>Workflow</TableHead>
<TableHead>Current Step</TableHead>
<TableHead>Requested At</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{approvals.myPendingApprovals.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className='text-muted-foreground text-center'>
No pending approvals are assigned to your current approver scope.
</TableCell>
</TableRow>
) : (
approvals.myPendingApprovals.map((row) => (
<TableRow key={row.id}>
<TableCell>
<div className='font-medium'>{row.entityCode ?? 'Approval'}</div>
<div className='font-medium'>{row.entityCode ?? 'อนุมัติเอกสาร'}</div>
<div className='text-muted-foreground text-xs'>{row.entityTitle ?? '-'}</div>
</TableCell>
<TableCell>{row.workflowName}</TableCell>

View File

@@ -61,11 +61,11 @@ export function DashboardFilters({
value={params.branch ?? '__all__'}
onValueChange={(value) => void setParams({ branch: value === '__all__' ? null : value })}
>
<SelectTrigger>
<SelectValue placeholder='Branch' />
<SelectTrigger>
<SelectValue placeholder='สาขา' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__all__'>All Branches</SelectItem>
<SelectItem value='__all__'></SelectItem>
{referenceData.branches.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
@@ -77,11 +77,11 @@ export function DashboardFilters({
value={params.salesman ?? '__all__'}
onValueChange={(value) => void setParams({ salesman: value === '__all__' ? null : value })}
>
<SelectTrigger>
<SelectValue placeholder='Sales Person' />
<SelectTrigger>
<SelectValue placeholder='ฝ่ายขาย' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__all__'>All Sales</SelectItem>
<SelectItem value='__all__'></SelectItem>
{referenceData.salesmen.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
@@ -95,11 +95,11 @@ export function DashboardFilters({
void setParams({ projectPartyRole: value === '__all__' ? null : value })
}
>
<SelectTrigger>
<SelectValue placeholder='Project Party Role' />
<SelectTrigger>
<SelectValue placeholder='บทบาทผู้เกี่ยวข้องในโครงการ' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__all__'>All Party Roles</SelectItem>
<SelectItem value='__all__'></SelectItem>
{referenceData.projectPartyRoles.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
@@ -111,11 +111,11 @@ export function DashboardFilters({
value={params.productType ?? '__all__'}
onValueChange={(value) => void setParams({ productType: value === '__all__' ? null : value })}
>
<SelectTrigger>
<SelectValue placeholder='Product Type' />
<SelectTrigger>
<SelectValue placeholder='ประเภทสินค้า' />
</SelectTrigger>
<SelectContent>
<SelectItem value='__all__'>All Product Types</SelectItem>
<SelectItem value='__all__'></SelectItem>
{referenceData.productTypes.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
@@ -138,7 +138,7 @@ export function DashboardFilters({
})
}
>
Reset Filters
</Button>
{canExport ? (
<>
@@ -148,7 +148,7 @@ export function DashboardFilters({
exportQuery ? `&${exportQuery}` : ''
}`}
>
<Icons.download className='mr-2 h-4 w-4' /> Export Sales CSV
<Icons.download className='mr-2 h-4 w-4' /> CSV
</Link>
</Button>
<Button asChild variant='outline'>
@@ -157,7 +157,7 @@ export function DashboardFilters({
exportQuery ? `&${exportQuery}` : ''
}`}
>
<Icons.download className='mr-2 h-4 w-4' /> Export Revenue Excel
<Icons.download className='mr-2 h-4 w-4' /> Excel
</Link>
</Button>
<Button asChild variant='outline'>
@@ -166,7 +166,7 @@ export function DashboardFilters({
exportQuery ? `&${exportQuery}` : ''
}`}
>
<Icons.download className='mr-2 h-4 w-4' /> Export Funnel CSV
<Icons.download className='mr-2 h-4 w-4' /> CSV Funnel
</Link>
</Button>
</>

View File

@@ -21,31 +21,31 @@ export function DashboardFollowups({
return (
<Card>
<CardHeader>
<CardTitle>Follow-up Dashboard</CardTitle>
<CardDescription>Upcoming and completed CRM follow-ups across enquiries and quotations.</CardDescription>
<CardTitle>Dashboard </CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-6'>
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-4'>
<Stat label='Due Today' value={followups.summary.dueToday} />
<Stat label='Due This Week' value={followups.summary.dueThisWeek} />
<Stat label='Overdue' value={followups.summary.overdue} />
<Stat label='Completed' value={followups.summary.completed} />
<Stat label='ครบกำหนดวันนี้' value={followups.summary.dueToday} />
<Stat label='ครบกำหนดสัปดาห์นี้' value={followups.summary.dueThisWeek} />
<Stat label='เกินกำหนด' value={followups.summary.overdue} />
<Stat label='เสร็จแล้ว' value={followups.summary.completed} />
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Enquiry</TableHead>
<TableHead>Enquiry Owner</TableHead>
<TableHead>Source</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{followups.upcoming.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className='text-muted-foreground text-center'>
No upcoming follow-ups match the current filters.
</TableCell>
</TableRow>
) : (

View File

@@ -13,8 +13,8 @@ export function DashboardFunnel({ steps }: { steps: CrmDashboardFunnelStep[] })
return (
<Card>
<CardHeader>
<CardTitle>Sales Pipeline Funnel</CardTitle>
<CardDescription>Lead, enquiry, quotation, approval, and won stages from the current pipeline model.</CardDescription>
<CardTitle>Funnel </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{steps.map((step) => (
@@ -23,12 +23,12 @@ export function DashboardFunnel({ steps }: { steps: CrmDashboardFunnelStep[] })
<div>
<div className='font-medium'>{step.label}</div>
<div className='text-muted-foreground text-xs'>
{formatNumber(step.count)} items, value {formatNumber(step.value)}
{formatNumber(step.count)} , {formatNumber(step.value)}
</div>
</div>
<div className='text-right text-sm'>
<div>{step.conversionRate === null ? '-' : `${formatNumber(step.conversionRate)}%`}</div>
<div className='text-muted-foreground text-xs'>conversion</div>
<div className='text-muted-foreground text-xs'></div>
</div>
</div>
<div className='bg-muted h-2 overflow-hidden rounded-full'>

View File

@@ -12,25 +12,25 @@ export function DashboardHotProjects({ rows }: { rows: CrmDashboardHotProjectRow
return (
<Card>
<CardHeader>
<CardTitle>Top Active Enquiries</CardTitle>
<CardDescription>Sales-owned hot enquiries ranked by estimated commercial value.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Customer</TableHead>
<TableHead className='text-right'>Value</TableHead>
<TableHead>Enquiry Owner</TableHead>
<TableHead className='text-right'>Probability</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead></TableHead>
<TableHead className='text-right'></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className='text-muted-foreground text-center'>
No hot opportunities match the current filters.
</TableCell>
</TableRow>
) : (

View File

@@ -28,16 +28,16 @@ function RevenueTable({
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead className='text-right'>Revenue</TableHead>
<TableHead className='text-right'>Quotations</TableHead>
<TableHead className='text-right'>Won Revenue</TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className='text-muted-foreground text-center'>
No revenue rows match the current filters.
</TableCell>
</TableRow>
) : (
@@ -68,23 +68,23 @@ export function DashboardRevenueCards({
return (
<div className='grid gap-4 xl:grid-cols-2'>
<RevenueTable
title='Top End Customers'
description='Revenue owner attribution with billing-customer fallback when end-customer is missing.'
title='เจ้าของโครงการมูลค่าสูงสุด'
description='จัดอันดับตามเจ้าของโครงการ โดยใช้ลูกค้าผู้รับใบเสนอราคาเป็น fallback เมื่อไม่มี end customer'
rows={revenueAnalytics.topEndCustomers}
/>
<RevenueTable
title='Top Billing Customers'
description='Commercial relationship view grouped by billing customer.'
title='ลูกค้าผู้รับใบเสนอราคามูลค่าสูงสุด'
description='มุมมองเชิงพาณิชย์แบบจัดกลุ่มตามลูกค้าผู้รับใบเสนอราคา'
rows={revenueAnalytics.topBillingCustomers}
/>
<RevenueTable
title='Top Contractors'
description='Relationship analytics with full attribution for each contractor role.'
title='ผู้รับเหมามูลค่าสูงสุด'
description='วิเคราะห์มูลค่าตามบทบาทผู้รับเหมาในโครงการ'
rows={revenueAnalytics.topContractors}
/>
<RevenueTable
title='Top Consultants'
description='Consultant-side revenue relationships from project parties.'
title='ที่ปรึกษามูลค่าสูงสุด'
description='วิเคราะห์มูลค่าตามบทบาทที่ปรึกษาในโครงการ'
rows={revenueAnalytics.topConsultants}
/>
</div>

View File

@@ -12,27 +12,27 @@ export function DashboardSalesRanking({ rows }: { rows: CrmDashboardSalesRanking
return (
<Card>
<CardHeader>
<CardTitle>Sales Ranking</CardTitle>
<CardDescription>Lead and enquiry ownership follow assignment rules. Quotation ownership uses salesman.</CardDescription>
<CardTitle></CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Sales Person</TableHead>
<TableHead className='text-right'>Lead</TableHead>
<TableHead className='text-right'>Enquiry</TableHead>
<TableHead className='text-right'>Quotation</TableHead>
<TableHead className='text-right'>Approved</TableHead>
<TableHead className='text-right'>Won Revenue</TableHead>
<TableHead className='text-right'>Conversion</TableHead>
<TableHead></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
<TableHead className='text-right'></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className='text-muted-foreground text-center'>
No sales ranking rows match the current filters.
</TableCell>
</TableRow>
) : (

View File

@@ -52,47 +52,47 @@ export function DashboardSummaryCards({
return (
<div className={`grid gap-4 ${canViewCommercialData ? 'xl:grid-cols-2 2xl:grid-cols-4' : 'xl:grid-cols-2'}`}>
<SummaryCard
title='Lead'
description='Marketing-owned lead-stage records based on pipeline stage.'
title='ลีด'
description='รายการที่ยังอยู่ในขั้นลีดและอยู่ในความรับผิดชอบของการตลาด'
href='/dashboard/crm/leads'
items={[
{ label: 'Lead Count', value: summary.lead.leadCount },
{ label: 'New Leads', value: summary.lead.newLeads },
{ label: 'Unassigned Leads', value: summary.lead.unassignedLeads },
{ label: 'Lead Aging Avg', value: summary.lead.averageLeadAgingDays, suffix: 'days' }
{ label: 'จำนวนลีด', value: summary.lead.leadCount },
{ label: 'ลีดใหม่', value: summary.lead.newLeads },
{ label: 'ลีดยังไม่มอบหมาย', value: summary.lead.unassignedLeads },
{ label: 'อายุลีดเฉลี่ย', value: summary.lead.averageLeadAgingDays, suffix: 'วัน' }
]}
/>
<SummaryCard
title='Enquiry'
description='Sales-owned enquiry-stage records plus closed outcomes.'
title='โอกาสขาย'
description='รายการที่อยู่ในขั้นโอกาสขาย รวมถึงผลลัพธ์ชนะและแพ้'
href='/dashboard/crm/enquiries'
items={[
{ label: 'Enquiry Count', value: summary.enquiry.enquiryCount },
{ label: 'Active Enquiries', value: summary.enquiry.activeEnquiries },
{ label: 'Closed Won', value: summary.enquiry.wonCount },
{ label: 'Closed Lost', value: summary.enquiry.lostCount }
{ label: 'จำนวนโอกาสขาย', value: summary.enquiry.enquiryCount },
{ label: 'โอกาสขายที่กำลังดำเนินการ', value: summary.enquiry.activeEnquiries },
{ label: 'จำนวนงานที่ชนะ', value: summary.enquiry.wonCount },
{ label: 'จำนวนงานที่แพ้', value: summary.enquiry.lostCount }
]}
/>
{canViewCommercialData ? (
<SummaryCard
title='Quotation'
description='Live quotation status counts from production data.'
title='ใบเสนอราคา'
description='สถานะของใบเสนอราคาจากข้อมูลจริงในระบบ'
items={[
{ label: 'Draft Quotations', value: summary.quotation.draftQuotations },
{ label: 'Pending Approval', value: summary.quotation.pendingApproval },
{ label: 'Approved Quotations', value: summary.quotation.approvedQuotations },
{ label: 'Sent Quotations', value: summary.quotation.sentQuotations }
{ label: 'ฉบับร่าง', value: summary.quotation.draftQuotations },
{ label: 'รออนุมัติ', value: summary.quotation.pendingApproval },
{ label: 'อนุมัติแล้ว', value: summary.quotation.approvedQuotations },
{ label: 'ส่งแล้ว', value: summary.quotation.sentQuotations }
]}
/>
) : null}
{canViewCommercialData ? (
<SummaryCard
title='Revenue'
description='Quotation, won, and lost values using governed revenue rules.'
title='มูลค่าใบเสนอราคา'
description='มูลค่าใบเสนอราคา งานที่ชนะ และงานที่แพ้ตามกติกา revenue attribution'
items={[
{ label: 'Quotation Value', value: summary.revenue.quotationValue },
{ label: 'Won Value', value: summary.revenue.wonValue },
{ label: 'Lost Value', value: summary.revenue.lostValue }
{ label: 'มูลค่าใบเสนอราคา', value: summary.revenue.quotationValue },
{ label: 'มูลค่างานที่ชนะ', value: summary.revenue.wonValue },
{ label: 'มูลค่างานที่แพ้', value: summary.revenue.lostValue }
]}
/>
) : null}

View File

@@ -30,6 +30,7 @@ import {
getRevenueByContractor,
getRevenueByEndCustomer
} from '@/features/crm/reporting/server/service';
import { getPipelineStageThaiLabel, getProjectPartyRoleThaiLabel } from '@/features/crm/shared/terminology';
import type {
CrmDashboardApprovalRow,
CrmDashboardApprovalSummary,
@@ -332,19 +333,19 @@ function buildFunnel(
const steps = [
{
key: 'lead',
label: 'Lead',
label: getPipelineStageThaiLabel('lead'),
count: summary.lead.leadCount,
value: 0
},
{
key: 'enquiry',
label: 'Enquiry',
label: getPipelineStageThaiLabel('enquiry'),
count: summary.enquiry.enquiryCount,
value: summary.enquiry.enquiryValue
},
{
key: 'quotation',
label: 'Quotation',
label: 'ใบเสนอราคา',
count: scopedQuotations.filter(
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled'
).length,
@@ -354,7 +355,7 @@ function buildFunnel(
},
{
key: 'pending_approval',
label: 'Pending Approval',
label: 'รออนุมัติ',
count: scopedQuotations.filter(
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'pending_approval'
).length,
@@ -364,7 +365,7 @@ function buildFunnel(
},
{
key: 'approved',
label: 'Approved',
label: 'อนุมัติแล้ว',
count: scopedQuotations.filter(
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'approved'
).length,
@@ -374,7 +375,7 @@ function buildFunnel(
},
{
key: 'closed_won',
label: 'Closed Won',
label: getPipelineStageThaiLabel('closed_won'),
count: summary.enquiry.wonCount,
value: scopedQuotations
.filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'accepted')

View File

@@ -57,21 +57,21 @@ export function EnquiryAssignmentDialog({
const assignMutation = useMutation({
...assignEnquiryMutation,
onSuccess: () => {
toast.success('Lead converted to enquiry successfully');
toast.success('ส่งต่อลีดไปยังฝ่ายขายสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to assign enquiry')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถมอบหมายโอกาสขายได้')
});
const reassignMutation = useMutation({
...reassignEnquiryMutation,
onSuccess: () => {
toast.success('Enquiry reassigned successfully');
toast.success('เปลี่ยนผู้ดูแลโอกาสขายสำเร็จ');
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to reassign enquiry')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถเปลี่ยนผู้ดูแลโอกาสขายได้')
});
const form = useAppForm({
@@ -109,11 +109,11 @@ export function EnquiryAssignmentDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{mode === 'assign' ? 'Convert To Enquiry' : 'Reassign Enquiry Owner'}</DialogTitle>
<DialogTitle>{mode === 'assign' ? 'ส่งต่อให้ฝ่ายขาย' : 'เปลี่ยนผู้ดูแลโอกาสขาย'}</DialogTitle>
<DialogDescription>
{mode === 'assign'
? 'Assign this lead to sales and convert it into an enquiry.'
: 'Move this enquiry to a different sales owner and keep the handoff note visible.'}
? 'มอบหมายลีดนี้ให้ฝ่ายขายเพื่อเปลี่ยนเป็นโอกาสขาย'
: 'เปลี่ยนผู้ดูแลโอกาสขายและเก็บบันทึกการส่งต่องานไว้'}
</DialogDescription>
</DialogHeader>
@@ -121,24 +121,24 @@ export function EnquiryAssignmentDialog({
<form.Form id='enquiry-assignment-form' className='px-0 md:px-0'>
{noUsersAvailable ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
No assignable sales users are available in this workspace yet.
workspace
</div>
) : (
<>
<FormSelectField
name='assignedToUserId'
label='Enquiry Owner'
label='ผู้ดูแลโอกาสขาย'
required
options={referenceData.assignableUsers.map((user) => ({
value: user.id,
label: `${user.name} (${user.businessRole.replaceAll('_', ' ')})`
}))}
placeholder='Select sales user'
placeholder='เลือกผู้ใช้ฝ่ายขาย'
/>
<FormTextareaField
name='remark'
label='Remark'
placeholder='Optional handoff context or assignment note'
label='หมายเหตุ'
placeholder='บริบทเพิ่มเติมหรือหมายเหตุประกอบการส่งต่องาน'
/>
</>
)}
@@ -147,7 +147,7 @@ export function EnquiryAssignmentDialog({
<DialogFooter>
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
type='submit'
@@ -156,7 +156,7 @@ export function EnquiryAssignmentDialog({
disabled={noUsersAvailable}
>
<Icons.userPen className='mr-2 h-4 w-4' />
{mode === 'assign' ? 'Convert To Enquiry' : 'Reassign Owner'}
{mode === 'assign' ? 'ส่งต่อให้ฝ่ายขาย' : 'เปลี่ยนผู้ดูแล'}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -42,14 +42,14 @@ export function EnquiryCellAction({
const deleteMutation = useMutation({
...deleteEnquiryMutation,
onSuccess: () => {
toast.success(`${workspace === 'lead' ? 'Lead' : 'Enquiry'} deleted successfully`);
toast.success(`${workspace === 'lead' ? 'ลบลีดสำเร็จ' : 'ลบโอกาสขายสำเร็จ'}`);
setDeleteOpen(false);
},
onError: (error) =>
toast.error(
error instanceof Error
? error.message
: `Failed to delete ${workspace === 'lead' ? 'lead' : 'enquiry'}`
: `${workspace === 'lead' ? 'ไม่สามารถลบลีดได้' : 'ไม่สามารถลบโอกาสขายได้'}`
)
});
@@ -76,7 +76,7 @@ export function EnquiryCellAction({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
router.push(
@@ -86,13 +86,13 @@ export function EnquiryCellAction({
)
}
>
<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' /> {workspace === 'lead' ? 'Edit Lead' : 'Edit Enquiry'}
<Icons.edit className='mr-2 h-4 w-4' /> {workspace === 'lead' ? 'แก้ไขลีด' : 'แก้ไขโอกาสขาย'}
</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

@@ -203,11 +203,11 @@ function getEnquiryWorkspaceColumns({
id: 'customer',
accessorFn: (row) => row.customerId,
header: ({ column }: { column: Column<EnquiryListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Billing Customer' />
<DataTableColumnHeader column={column} title='ลูกค้าผู้รับใบเสนอราคา' />
),
cell: ({ row }) => <span>{row.original.customerName}</span>,
meta: {
label: 'Billing Customer',
label: 'ลูกค้าผู้รับใบเสนอราคา',
variant: 'multiSelect' as const,
options: referenceData.customers.map((item) => ({
value: item.id,

View File

@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { QuotationRelationItem } from '@/features/crm/quotations/api/types';
import { getPipelineStageThaiLabel } from '@/features/crm/shared/terminology';
import { enquiryByIdOptions, enquiryProjectPartiesOptions } from '../api/queries';
import type { EnquiryRecord, EnquiryReferenceData } from '../api/types';
import { EnquiryAssignmentDialog } from './enquiry-assignment-dialog';
@@ -27,18 +28,7 @@ function FieldItem({ label, value }: { label: string; value: string | null | und
}
function getPipelineStageLabel(stage: EnquiryRecord['pipelineStage']) {
switch (stage) {
case 'lead':
return 'Lead';
case 'enquiry':
return 'Enquiry';
case 'closed_won':
return 'Closed Won';
case 'closed_lost':
return 'Closed Lost';
default:
return stage;
}
return getPipelineStageThaiLabel(stage);
}
interface EnquiryDetailProps {
@@ -109,26 +99,26 @@ export function EnquiryDetail({
const pipelineStageLabel = getPipelineStageLabel(enquiry.pipelineStage);
const isLeadWorkspace = workspace === 'lead';
const backHref = isLeadWorkspace ? '/dashboard/crm/leads' : '/dashboard/crm/enquiries';
const recordLabel = isLeadWorkspace ? 'Lead' : 'Enquiry';
const ownerLabel = isLeadWorkspace ? 'Assigned Sales' : 'Enquiry Owner';
const infoCardTitle = isLeadWorkspace ? 'Lead Information' : 'Enquiry Information';
const recordLabel = isLeadWorkspace ? 'ลีด' : 'โอกาสขาย';
const ownerLabel = isLeadWorkspace ? 'ฝ่ายขายที่รับผิดชอบ' : 'ผู้ดูแลโอกาสขาย';
const infoCardTitle = isLeadWorkspace ? 'ข้อมูลลีด' : 'ข้อมูลโอกาสขาย';
const infoCardDescription = isLeadWorkspace
? 'Lead source, qualification context, and sales handoff readiness.'
: 'Project scope, ownership context, and active sales execution details.';
? 'ที่มาของลีด ข้อมูลการคัดกรอง และความพร้อมในการส่งต่อให้ฝ่ายขาย'
: 'ขอบเขตงาน ข้อมูลผู้รับผิดชอบ และรายละเอียดการดูแลของฝ่ายขาย';
const assignmentButtonLabel =
assignmentMode === 'assign'
? isLeadWorkspace
? 'Assign To Sales'
: 'Assign Enquiry Owner'
? 'ส่งต่อให้ฝ่ายขาย'
: 'มอบหมายผู้ดูแลโอกาสขาย'
: isLeadWorkspace
? 'Reassign Sales Owner'
: 'Reassign Enquiry Owner';
const projectSectionLabel = isLeadWorkspace ? 'Lead Scope' : 'Project Information';
const followupTabLabel = isLeadWorkspace ? 'Lead Follow-ups' : 'Follow-ups';
const relatedTabLabel = isLeadWorkspace ? 'Related Quotations' : 'Quotations';
? 'เปลี่ยนฝ่ายขายที่รับผิดชอบ'
: 'เปลี่ยนผู้ดูแลโอกาสขาย';
const projectSectionLabel = isLeadWorkspace ? 'ขอบเขตลีด' : 'ข้อมูลโครงการ';
const followupTabLabel = isLeadWorkspace ? 'งานติดตามลีด' : 'งานติดตาม';
const relatedTabLabel = isLeadWorkspace ? 'ใบเสนอราคาที่เกี่ยวข้อง' : 'ใบเสนอราคา';
const snapshotDescription = isLeadWorkspace
? 'Operational metadata and lead ownership handoff state for this record.'
: 'Operational metadata and assignment state for this enquiry record.';
? 'ข้อมูลระบบและสถานะการส่งต่อของลีดรายการนี้'
: 'ข้อมูลระบบและสถานะการรับผิดชอบของโอกาสขายรายการนี้';
return (
<div className='space-y-6'>
@@ -147,7 +137,7 @@ export function EnquiryDetail({
<div>
<h2 className='text-2xl font-semibold'>{enquiry.title}</h2>
<p className='text-muted-foreground text-sm'>
{pipelineStageLabel} | {customer?.name ?? 'Unknown customer'}
{pipelineStageLabel} | {customer?.name ?? 'ไม่ทราบชื่อลูกค้า'}
{contact ? ` | ${contact.name}` : ''}
</p>
</div>
@@ -174,9 +164,9 @@ export function EnquiryDetail({
<CardContent className='pt-6'>
<Tabs defaultValue='overview' className='gap-4'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='overview'></TabsTrigger>
<TabsTrigger value='followups'>{followupTabLabel}</TabsTrigger>
<TabsTrigger value='activity'>Activity</TabsTrigger>
<TabsTrigger value='activity'></TabsTrigger>
{canViewRelatedQuotations ? (
<TabsTrigger value='related'>{relatedTabLabel}</TabsTrigger>
) : null}
@@ -189,29 +179,29 @@ export function EnquiryDetail({
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<FieldItem
label={isLeadWorkspace ? 'Company' : 'Billing Customer'}
label={isLeadWorkspace ? 'ลูกค้า' : 'ลูกค้าผู้รับใบเสนอราคา'}
value={customer?.name}
/>
<FieldItem label='Contact' value={contact?.name} />
<FieldItem label='ผู้ติดต่อ' value={contact?.name} />
<FieldItem
label='Branch'
label='สาขา'
value={enquiry.branchId ? branchMap.get(enquiry.branchId) : null}
/>
<FieldItem label='Active' value={enquiry.isActive ? 'Yes' : 'No'} />
<FieldItem label='ใช้งานอยู่' value={enquiry.isActive ? 'ใช่' : 'ไม่ใช่'} />
<FieldItem
label='Product Type'
label='ประเภทสินค้า'
value={productTypeMap.get(enquiry.productType)?.label}
/>
<FieldItem
label='Priority'
label='ความสำคัญ'
value={priorityMap.get(enquiry.priority)?.label}
/>
<FieldItem
label='Lead Source'
label='ที่มาของลีด'
value={leadChannelMap.get(enquiry.leadChannel ?? '')?.label}
/>
<FieldItem
label='Expected Close Date'
label='วันที่คาดว่าจะปิดการขาย'
value={
enquiry.expectedCloseDate
? new Date(enquiry.expectedCloseDate).toLocaleDateString()
@@ -219,7 +209,7 @@ export function EnquiryDetail({
}
/>
<FieldItem
label='Estimated Value'
label='มูลค่าประมาณการ'
value={
enquiry.estimatedValue !== null
? enquiry.estimatedValue.toLocaleString()
@@ -227,31 +217,31 @@ export function EnquiryDetail({
}
/>
<FieldItem
label='Chance %'
label='โอกาสปิดการขาย %'
value={enquiry.chancePercent !== null ? `${enquiry.chancePercent}%` : null}
/>
<FieldItem label='Competitor' value={enquiry.competitor} />
<FieldItem label='Source' value={enquiry.source} />
<FieldItem label='Pipeline Stage' value={pipelineStageLabel} />
<FieldItem label='คู่แข่ง' value={enquiry.competitor} />
<FieldItem label='ที่มา' value={enquiry.source} />
<FieldItem label='ขั้นตอนใน Pipeline' value={pipelineStageLabel} />
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
<FieldItem
label='Assigned At'
label='มอบหมายเมื่อ'
value={
enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null
}
/>
<FieldItem label='Assigned By' value={enquiry.assignedByName} />
<FieldItem label='Assignment Remark' value={enquiry.assignmentRemark} />
<FieldItem label='มอบหมายโดย' value={enquiry.assignedByName} />
<FieldItem label='หมายเหตุการมอบหมาย' value={enquiry.assignmentRemark} />
<div className='md:col-span-2 xl:col-span-4'>
<div className='space-y-3'>
<div className='text-muted-foreground text-xs'>
{isLeadWorkspace ? 'Lead Parties' : 'Project Parties'}
{isLeadWorkspace ? 'ผู้เกี่ยวข้องของลีด' : 'ผู้เกี่ยวข้องในโครงการ'}
</div>
{!projectPartiesData.items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
{isLeadWorkspace
? 'No lead parties have been added yet.'
: 'No project parties have been added yet.'}
? 'ยังไม่มีผู้เกี่ยวข้องของลีด'
: 'ยังไม่มีผู้เกี่ยวข้องในโครงการ'}
</div>
) : (
<div className='space-y-3'>
@@ -259,7 +249,7 @@ export function EnquiryDetail({
<div key={item.id} className='rounded-lg border p-4 text-sm'>
<div className='font-medium'>{item.customerName}</div>
<div className='text-muted-foreground'>
Role: {item.roleLabel ?? item.roleCode}
: {item.roleLabel ?? item.roleCode}
</div>
{item.remark ? <div className='mt-1'>{item.remark}</div> : null}
</div>
@@ -278,13 +268,13 @@ export function EnquiryDetail({
/>
</div>
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem label='Description' value={enquiry.description} />
<FieldItem label='รายละเอียด' value={enquiry.description} />
</div>
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem label='Requirement' value={enquiry.requirement} />
<FieldItem label='ความต้องการ' value={enquiry.requirement} />
</div>
<div className='md:col-span-2 xl:col-span-4'>
<FieldItem label='Notes' value={enquiry.notes} />
<FieldItem label='หมายเหตุ' value={enquiry.notes} />
</div>
</CardContent>
</Card>
@@ -302,15 +292,13 @@ export function EnquiryDetail({
<TabsContent value='activity'>
<Card>
<CardHeader>
<CardTitle>{isLeadWorkspace ? 'Lead Activity' : 'Activity'}</CardTitle>
<CardDescription>
Audit events for this record and its follow-up history.
</CardDescription>
<CardTitle>{isLeadWorkspace ? 'กิจกรรมของลีด' : 'กิจกรรม'}</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{!data.activity.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No activity recorded yet.
</div>
) : (
data.activity.map((item, index) => (
@@ -347,13 +335,13 @@ export function EnquiryDetail({
<CardHeader>
<CardTitle>{relatedTabLabel}</CardTitle>
<CardDescription>
Production quotations already linked to this record.
</CardDescription>
</CardHeader>
<CardContent>
{!relatedQuotations.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No quotations have been linked to this record yet.
</div>
) : (
<div className='space-y-3'>
@@ -388,21 +376,21 @@ export function EnquiryDetail({
<div className='space-y-6'>
<Card>
<CardHeader>
<CardTitle>Record Snapshot</CardTitle>
<CardTitle></CardTitle>
<CardDescription>{snapshotDescription}</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<FieldItem label='Pipeline Stage' value={pipelineStageLabel} />
<FieldItem label='ขั้นตอนใน Pipeline' value={pipelineStageLabel} />
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
<FieldItem label='Assigned By' value={enquiry.assignedByName} />
<FieldItem label='Created By' value={enquiry.createdBy} />
<FieldItem label='Updated By' value={enquiry.updatedBy} />
<FieldItem label='มอบหมายโดย' value={enquiry.assignedByName} />
<FieldItem label='สร้างโดย' value={enquiry.createdBy} />
<FieldItem label='อัปเดตโดย' value={enquiry.updatedBy} />
<FieldItem
label='Assigned At'
label='มอบหมายเมื่อ'
value={enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null}
/>
<FieldItem label='Created At' value={new Date(enquiry.createdAt).toLocaleString()} />
<FieldItem label='Updated At' value={new Date(enquiry.updatedAt).toLocaleString()} />
<FieldItem label='สร้างเมื่อ' value={new Date(enquiry.createdAt).toLocaleString()} />
<FieldItem label='อัปเดตเมื่อ' value={new Date(enquiry.updatedAt).toLocaleString()} />
</CardContent>
</Card>
</div>

View File

@@ -38,22 +38,20 @@ export function EnquiryFollowupsTab({
const deleteMutation = useMutation({
...deleteEnquiryFollowupMutation,
onSuccess: () => {
toast.success('Follow-up deleted successfully');
toast.success('ลบงานติดตามสำเร็จ');
setDeleteOpen(false);
setSelectedId(null);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to delete follow-up')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบงานติดตามได้')
});
return (
<Card>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div>
<CardTitle>Follow-ups</CardTitle>
<CardDescription>
Track the latest contact moments, outcomes, and next steps for this enquiry.
</CardDescription>
<CardTitle></CardTitle>
<CardDescription> </CardDescription>
</div>
{canCreate ? (
<Button
@@ -63,14 +61,14 @@ export function EnquiryFollowupsTab({
setOpen(true);
}}
>
<Icons.add className='mr-2 h-4 w-4' /> Add Follow-up
<Icons.add className='mr-2 h-4 w-4' />
</Button>
) : null}
</CardHeader>
<CardContent className='space-y-4'>
{!data.items.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No follow-ups recorded yet.
</div>
) : (
data.items.map((item) => (
@@ -89,11 +87,11 @@ export function EnquiryFollowupsTab({
</div>
{item.outcome ? <p className='text-sm'>{item.outcome}</p> : null}
{item.nextAction ? (
<p className='text-muted-foreground text-sm'>Next action: {item.nextAction}</p>
<p className='text-muted-foreground text-sm'>: {item.nextAction}</p>
) : null}
{item.nextFollowupDate ? (
<p className='text-muted-foreground text-sm'>
Next follow-up: {new Date(item.nextFollowupDate).toLocaleDateString()}
: {new Date(item.nextFollowupDate).toLocaleDateString()}
</p>
) : null}
{item.notes ? <p className='text-muted-foreground text-sm'>{item.notes}</p> : null}
@@ -108,7 +106,7 @@ export function EnquiryFollowupsTab({
setOpen(true);
}}
>
<Icons.edit className='mr-2 h-4 w-4' /> Edit
<Icons.edit className='mr-2 h-4 w-4' />
</Button>
<Button
variant='outline'
@@ -119,7 +117,7 @@ export function EnquiryFollowupsTab({
setDeleteOpen(true);
}}
>
<Icons.trash className='mr-2 h-4 w-4' /> Delete
<Icons.trash className='mr-2 h-4 w-4' />
</Button>
</div>
</div>

View File

@@ -90,21 +90,21 @@ export function EnquiryFormSheet({
const createMutation = useMutation({
...createEnquiryMutation,
onSuccess: () => {
toast.success(`${recordLabel} created successfully`);
toast.success(`สร้าง${recordLabel}สำเร็จ`);
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : `Failed to create ${recordLabel.toLowerCase()}`)
toast.error(error instanceof Error ? error.message : `ไม่สามารถสร้าง${recordLabel}ได้`)
});
const updateMutation = useMutation({
...updateEnquiryMutation,
onSuccess: () => {
toast.success(`${recordLabel} updated successfully`);
toast.success(`อัปเดต${recordLabel}สำเร็จ`);
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : `Failed to update ${recordLabel.toLowerCase()}`)
toast.error(error instanceof Error ? error.message : `ไม่สามารถอัปเดต${recordLabel}ได้`)
});
const contactsForCustomer = referenceData.contacts.filter(
@@ -179,17 +179,17 @@ export function EnquiryFormSheet({
const isPending = createMutation.isPending || updateMutation.isPending;
const recordLabel = workspace === 'lead' ? 'Lead' : 'Enquiry';
const recordLabel = workspace === 'lead' ? 'ลีด' : 'โอกาสขาย';
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className='flex flex-col sm:max-w-4xl'>
<SheetHeader>
<SheetTitle>{isEdit ? `Edit ${recordLabel}` : `New ${recordLabel}`}</SheetTitle>
<SheetTitle>{isEdit ? `แก้ไข${recordLabel}` : `เพิ่ม${recordLabel}`}</SheetTitle>
<SheetDescription>
{workspace === 'lead'
? 'Capture marketing lead details before assignment to sales.'
: 'Capture sales enquiry details before quotation work begins.'}
? 'บันทึกข้อมูลลีดของฝ่ายการตลาดก่อนส่งต่อให้ฝ่ายขาย'
: 'บันทึกข้อมูลโอกาสขายของฝ่ายขายก่อนเริ่มทำใบเสนอราคา'}
</SheetDescription>
</SheetHeader>
@@ -201,7 +201,7 @@ export function EnquiryFormSheet({
children={(field) => (
<field.FieldSet>
<field.Field>
<field.FieldLabel>Billing Customer *</field.FieldLabel>
<field.FieldLabel> *</field.FieldLabel>
<Select
value={field.state.value ?? ''}
onValueChange={(value) => {
@@ -211,8 +211,8 @@ export function EnquiryFormSheet({
form.setFieldValue('contactId', '');
}}
>
<SelectTrigger aria-label='Customer'>
<SelectValue placeholder='Select customer' />
<SelectTrigger aria-label='ลูกค้า'>
<SelectValue placeholder='เลือกลูกค้า' />
</SelectTrigger>
<SelectContent>
{referenceData.customers.map((customer) => (
@@ -233,7 +233,7 @@ export function EnquiryFormSheet({
children={(field) => (
<field.FieldSet>
<field.Field>
<field.FieldLabel>Contact</field.FieldLabel>
<field.FieldLabel></field.FieldLabel>
<Select
value={field.state.value ?? ''}
onValueChange={(value) => {
@@ -241,19 +241,19 @@ export function EnquiryFormSheet({
field.handleBlur();
}}
>
<SelectTrigger aria-label='Contact'>
<SelectValue placeholder='Select contact' />
<SelectTrigger aria-label='ผู้ติดต่อ'>
<SelectValue placeholder='เลือกผู้ติดต่อ' />
</SelectTrigger>
<SelectContent>
{contactsForCustomer.length ? (
contactsForCustomer.map((contact) => (
<SelectItem key={contact.id} value={contact.id}>
{contact.name}
{contact.isPrimary ? ' (Primary)' : ''}
{contact.isPrimary ? ' (ผู้ติดต่อหลัก)' : ''}
</SelectItem>
))
) : (
<SelectItem value='__none__'>No contact selected</SelectItem>
<SelectItem value='__none__'></SelectItem>
)}
</SelectContent>
</Select>
@@ -265,28 +265,28 @@ export function EnquiryFormSheet({
<FormTextField
name='title'
label='Title'
label='ชื่อรายการ'
required
placeholder='Warehouse crane upgrade enquiry'
/>
<FormTextField
name='projectName'
label='Project Name'
label='ชื่อโครงการ'
placeholder='Bangna Warehouse Expansion'
/>
<FormTextField
name='projectLocation'
label='Project Location'
label='สถานที่โครงการ'
placeholder='Bangkok'
/>
<FormTextField
name='source'
label='Source'
label='ที่มา'
placeholder='Existing customer referral'
/>
<FormSelectField
name='branchId'
label='Branch'
label='สาขา'
options={referenceData.branches.map((branch) => ({
value: branch.id,
label: branch.name
@@ -294,7 +294,7 @@ export function EnquiryFormSheet({
/>
<FormSelectField
name='productType'
label='Product Type'
label='ประเภทสินค้า'
required
options={referenceData.productTypes.map((item) => ({
value: item.id,
@@ -303,7 +303,7 @@ export function EnquiryFormSheet({
/>
<FormSelectField
name='status'
label='Status'
label='สถานะ'
required
options={referenceData.statuses.map((item) => ({
value: item.id,
@@ -312,7 +312,7 @@ export function EnquiryFormSheet({
/>
<FormSelectField
name='priority'
label='Priority'
label='ความสำคัญ'
required
options={referenceData.priorities.map((item) => ({
value: item.id,
@@ -321,7 +321,7 @@ export function EnquiryFormSheet({
/>
<FormSelectField
name='leadChannel'
label='Lead Channel'
label='ที่มาของลีด'
options={referenceData.leadChannels.map((item) => ({
value: item.id,
label: item.label
@@ -329,48 +329,48 @@ export function EnquiryFormSheet({
/>
<FormTextField
name='estimatedValue'
label='Estimated Value'
label='มูลค่าประมาณการ'
type='number'
placeholder='2500000'
/>
<FormTextField name='chancePercent' label='Chance %' type='number' placeholder='60' />
<FormTextField name='chancePercent' label='โอกาสปิดการขาย %' type='number' placeholder='60' />
<FormTextField
name='expectedCloseDate'
label='Expected Close Date'
label='วันที่คาดว่าจะปิดการขาย'
placeholder='YYYY-MM-DD'
/>
<FormTextField name='competitor' label='Competitor' placeholder='Other vendor name' />
<FormTextField name='competitor' label='คู่แข่ง' placeholder='Other vendor name' />
<div className='md:col-span-2'>
<FormTextareaField
name='description'
label='Description'
placeholder='High-level summary of the lead or enquiry'
label='รายละเอียด'
placeholder='สรุปภาพรวมของลีดหรือโอกาสขาย'
/>
</div>
<div className='md:col-span-2'>
<FormTextareaField
name='requirement'
label='Requirement'
placeholder='Specific technical or commercial requirements'
label='ความต้องการ'
placeholder='ความต้องการเชิงเทคนิคหรือเชิงพาณิชย์'
/>
</div>
<div className='md:col-span-2'>
<FormTextareaField
name='notes'
label='Notes'
placeholder='Internal notes, blockers, or handoff guidance'
label='หมายเหตุ'
placeholder='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน'
/>
</div>
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
<FormSwitchField
name='isHotProject'
label='Hot Project'
description='Use for urgent or highly strategic enquiries.'
label='โครงการด่วน'
description='ใช้สำหรับงานเร่งด่วนหรือโครงการสำคัญเชิงกลยุทธ์'
/>
<FormSwitchField
name='isActive'
label='Active Record'
description='Inactive leads and enquiries stay in history but should not progress further.'
label='ใช้งานอยู่'
description='รายการที่ปิดใช้งานจะยังอยู่ในประวัติ แต่ไม่ควรถูกดำเนินการต่อ'
/>
</div>
<div className='md:col-span-2'>
@@ -387,11 +387,11 @@ export function EnquiryFormSheet({
<SheetFooter>
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type='submit' form='enquiry-form-sheet' isLoading={isPending}>
<Icons.check className='mr-2 h-4 w-4' />
{isEdit ? `Update ${recordLabel}` : `Create ${recordLabel}`}
{isEdit ? `อัปเดต${recordLabel}` : `สร้าง${recordLabel}`}
</Button>
</SheetFooter>
</SheetContent>
@@ -411,7 +411,7 @@ export function EnquiryFormSheetTrigger({
return (
<>
<Button onClick={() => setOpen(true)}>
<Icons.add className='mr-2 h-4 w-4' /> {workspace === 'lead' ? 'Add Lead' : 'Add Enquiry'}
<Icons.add className='mr-2 h-4 w-4' /> {workspace === 'lead' ? 'เพิ่มลีด' : 'เพิ่มโอกาสขาย'}
</Button>
<EnquiryFormSheet
open={open}

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) => {

View File

@@ -1,3 +1,5 @@
import { getProjectPartyRoleThaiLabel } from './terminology';
export const PROJECT_PARTY_OPTION_CATEGORY = 'crm_project_party_role';
const LEGACY_PROJECT_PARTY_ROLE_CODE_MAP = {
@@ -8,12 +10,12 @@ const LEGACY_PROJECT_PARTY_ROLE_CODE_MAP = {
} as const;
export const PROJECT_PARTY_CODE_LABELS: Record<string, string> = {
billing_customer: 'Billing Customer',
customer: 'Customer',
consultant: 'Consultant',
contractor: 'Contractor',
end_customer: 'End Customer',
other: 'Other'
billing_customer: getProjectPartyRoleThaiLabel('billing_customer'),
customer: getProjectPartyRoleThaiLabel('customer'),
consultant: getProjectPartyRoleThaiLabel('consultant'),
contractor: getProjectPartyRoleThaiLabel('contractor'),
end_customer: getProjectPartyRoleThaiLabel('end_customer'),
other: getProjectPartyRoleThaiLabel('other')
};
export interface ProjectPartyRoleOptionLike {

View File

@@ -0,0 +1,51 @@
export const CRM_TERMS = {
crm: 'CRM',
dashboard: 'Dashboard',
lead: 'ลีด',
leads: 'ลีด',
enquiry: 'โอกาสขาย',
enquiries: 'โอกาสขาย',
quotation: 'ใบเสนอราคา',
quotations: 'ใบเสนอราคา',
approval: 'อนุมัติเอกสาร',
approvals: 'อนุมัติเอกสาร',
customer: 'ลูกค้า',
customers: 'ลูกค้า',
contact: 'ผู้ติดต่อ',
contacts: 'ผู้ติดต่อ',
billingCustomer: 'ลูกค้าผู้รับใบเสนอราคา',
projectParties: 'ผู้เกี่ยวข้องในโครงการ',
crmSettings: 'ตั้งค่า CRM',
leadCount: 'จำนวนลีด',
enquiryCount: 'จำนวนโอกาสขาย',
wonCount: 'จำนวนงานที่ชนะ',
lostCount: 'จำนวนงานที่แพ้',
revenue: 'มูลค่าใบเสนอราคา',
conversionRate: 'อัตราการเปลี่ยนสถานะ'
} as const;
export const PIPELINE_STAGE_THAI_LABELS = {
lead: 'ลีด',
enquiry: 'โอกาสขาย',
closed_won: 'ชนะ',
closed_lost: 'แพ้'
} as const;
export const PROJECT_PARTY_ROLE_THAI_LABELS: Record<string, string> = {
billing_customer: 'ลูกค้าผู้รับใบเสนอราคา',
customer: 'ลูกค้า',
consultant: 'ที่ปรึกษา',
contractor: 'ผู้รับเหมา',
end_customer: 'เจ้าของโครงการ',
other: 'อื่น ๆ'
};
export function getProjectPartyRoleThaiLabel(roleCode: string, fallback?: string | null) {
return PROJECT_PARTY_ROLE_THAI_LABELS[roleCode] ?? fallback ?? roleCode;
}
export function getPipelineStageThaiLabel(
stage: keyof typeof PIPELINE_STAGE_THAI_LABELS | string
) {
return PIPELINE_STAGE_THAI_LABELS[stage as keyof typeof PIPELINE_STAGE_THAI_LABELS] ?? stage;
}

View File

@@ -13,7 +13,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
id: 'entityCode',
accessorFn: (row) => row.entityCode ?? row.entityId,
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Request' />
<DataTableColumnHeader column={column} title='คำขอ' />
),
cell: ({ row }) => (
<div className='flex flex-col'>
@@ -27,8 +27,8 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
</div>
),
meta: {
label: 'Request',
placeholder: 'Search approvals...',
label: 'คำขอ',
placeholder: 'ค้นหางานอนุมัติ...',
variant: 'text' as const,
icon: Icons.search
},
@@ -45,11 +45,11 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
label: 'Status',
variant: 'multiSelect' as const,
options: [
{ value: 'pending', label: 'Pending' },
{ value: 'approved', label: 'Approved' },
{ value: 'rejected', label: 'Rejected' },
{ value: 'returned', label: 'Returned' },
{ value: 'cancelled', label: 'Cancelled' }
{ value: 'pending', label: 'รออนุมัติ' },
{ value: 'approved', label: 'อนุมัติแล้ว' },
{ value: 'rejected', label: 'ไม่อนุมัติ' },
{ value: 'returned', label: 'ตีกลับ' },
{ value: 'cancelled', label: 'ยกเลิก' }
]
},
enableColumnFilter: true
@@ -58,13 +58,13 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
id: 'entityType',
accessorKey: 'entityType',
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Entity' />
<DataTableColumnHeader column={column} title='เอกสาร' />
),
cell: ({ row }) => row.original.entityType.replaceAll('_', ' '),
meta: {
label: 'Entity',
label: 'เอกสาร',
variant: 'multiSelect' as const,
options: [{ value: 'quotation', label: 'Quotation' }]
options: [{ value: 'quotation', label: 'ใบเสนอราคา' }]
},
enableColumnFilter: true
},
@@ -72,11 +72,11 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
id: 'currentStep',
accessorKey: 'currentStep',
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Current Step' />
<DataTableColumnHeader column={column} title='ขั้นตอนปัจจุบัน' />
),
cell: ({ row }) => (
<div className='flex flex-col'>
<span>Step {row.original.currentStep}</span>
<span> {row.original.currentStep}</span>
<span className='text-muted-foreground text-xs'>
{row.original.currentStepRoleName ?? '-'}
</span>
@@ -87,7 +87,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
id: 'requestedBy',
accessorFn: (row) => row.requestedByName ?? row.requestedBy,
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Requested By' />
<DataTableColumnHeader column={column} title='ผู้ส่งอนุมัติ' />
),
cell: ({ row }) => row.original.requestedByName ?? row.original.requestedBy
},
@@ -95,7 +95,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
id: 'requestedAt',
accessorKey: 'requestedAt',
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
<DataTableColumnHeader column={column} title='Requested At' />
<DataTableColumnHeader column={column} title='วันที่ส่งอนุมัติ' />
),
cell: ({ row }) => new Date(row.original.requestedAt).toLocaleString()
}

View File

@@ -60,35 +60,35 @@ export function ApprovalRequestPanel({
const approveAction = useMutation({
...approveApprovalMutation,
onSuccess: () => {
toast.success('Approval step completed');
toast.success('อนุมัติขั้นตอนนี้สำเร็จ');
setRemark('');
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Unable to approve request')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอนุมัติคำขอได้')
});
const rejectAction = useMutation({
...rejectApprovalMutation,
onSuccess: () => {
toast.success('Approval request rejected');
toast.success('ไม่อนุมัติคำขอสำเร็จ');
setRemark('');
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Unable to reject request')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถไม่อนุมัติคำขอได้')
});
const returnAction = useMutation({
...returnApprovalMutation,
onSuccess: () => {
toast.success('Approval request returned to draft');
toast.success('ตีกลับคำขอเพื่อแก้ไขสำเร็จ');
setRemark('');
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Unable to return request')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถตีกลับคำขอได้')
});
const cancelAction = useMutation({
...cancelApprovalMutation,
onSuccess: () => toast.success('Approval request cancelled'),
onSuccess: () => toast.success('ยกเลิกคำขออนุมัติสำเร็จ'),
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Unable to cancel request')
toast.error(error instanceof Error ? error.message : 'ไม่สามารถยกเลิกคำขอได้')
});
const isActing =
approveAction.isPending ||
@@ -100,30 +100,30 @@ export function ApprovalRequestPanel({
<div className='space-y-6'>
<Card>
<CardHeader>
<CardTitle>Request Information</CardTitle>
<CardDescription>Current workflow state for this approval request.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<FieldItem label='Workflow' value={approval.workflow.name} />
<div className='space-y-1'>
<div className='text-muted-foreground text-xs'>Status</div>
<div className='text-muted-foreground text-xs'></div>
<ApprovalStatusBadge status={approval.request.status} />
</div>
<FieldItem label='Entity Type' value={approval.request.entityType.replaceAll('_', ' ')} />
<FieldItem label='Entity Code' value={approval.entityCode ?? approval.request.entityId} />
<FieldItem label='Entity Title' value={approval.entityTitle} />
<FieldItem label='ประเภทเอกสาร' value={approval.request.entityType.replaceAll('_', ' ')} />
<FieldItem label='รหัสเอกสาร' value={approval.entityCode ?? approval.request.entityId} />
<FieldItem label='ชื่อเอกสาร' value={approval.entityTitle} />
<FieldItem
label='Requested At'
label='วันที่ส่งอนุมัติ'
value={new Date(approval.request.requestedAt).toLocaleString()}
/>
<FieldItem label='Requested By' value={approval.request.requestedBy} />
<FieldItem label='ผู้ส่งอนุมัติ' value={approval.request.requestedBy} />
<FieldItem
label='Current Step'
label='ขั้นตอนปัจจุบัน'
value={
approval.currentStep
? `Step ${approval.currentStep.stepNumber} - ${approval.currentStep.roleName}`
: approval.request.status === 'approved'
? 'Completed'
? 'เสร็จสิ้น'
: '-'
}
/>
@@ -132,7 +132,7 @@ export function ApprovalRequestPanel({
<Button asChild variant='outline'>
<Link href={entityHref}>
<Icons.arrowRight className='mr-2 h-4 w-4' />
Open Related Document
</Link>
</Button>
</div>
@@ -143,7 +143,7 @@ export function ApprovalRequestPanel({
<Card>
<CardHeader>
<CardTitle>Workflow</CardTitle>
<CardDescription>Sequential approver chain configured for this document.</CardDescription>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-3'>
{approval.steps.map((step) => {
@@ -157,12 +157,12 @@ export function ApprovalRequestPanel({
>
<div>
<div className='font-medium'>
Step {step.stepNumber} - {step.roleName}
{step.stepNumber} - {step.roleName}
</div>
<div className='text-muted-foreground text-sm'>{step.roleCode}</div>
</div>
<Badge variant={isCurrent ? 'secondary' : isCompleted ? 'default' : 'outline'}>
{isCurrent ? 'Current' : isCompleted ? 'Completed' : 'Pending'}
{isCurrent ? 'ปัจจุบัน' : isCompleted ? 'เสร็จสิ้น' : 'รอดำเนินการ'}
</Badge>
</div>
);
@@ -172,16 +172,16 @@ export function ApprovalRequestPanel({
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
<CardTitle></CardTitle>
<CardDescription>
Approvers can approve, reject, or return on their assigned step.
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<Textarea
value={remark}
onChange={(event) => setRemark(event.target.value)}
placeholder='Optional remark for approval history'
placeholder='หมายเหตุเพิ่มเติมสำหรับประวัติการอนุมัติ'
rows={4}
/>
<div className='flex flex-wrap gap-2'>
@@ -193,7 +193,7 @@ export function ApprovalRequestPanel({
}
>
<Icons.circleCheck className='mr-2 h-4 w-4' />
Approve
</Button>
) : null}
{canReject && canHandleCurrentStep && isPending ? (
@@ -205,7 +205,7 @@ export function ApprovalRequestPanel({
}
>
<Icons.warning className='mr-2 h-4 w-4' />
Reject
</Button>
) : null}
{canReturn && canHandleCurrentStep && isPending ? (
@@ -217,7 +217,7 @@ export function ApprovalRequestPanel({
}
>
<Icons.chevronLeft className='mr-2 h-4 w-4' />
Return
</Button>
) : null}
{canCancelCurrentRequest ? (
@@ -227,36 +227,36 @@ export function ApprovalRequestPanel({
onClick={() => cancelAction.mutate(approval.request.id)}
>
<Icons.close className='mr-2 h-4 w-4' />
Cancel Request
</Button>
) : null}
</div>
{!canHandleCurrentStep && isPending ? (
<div className='text-muted-foreground text-sm'>
Current step is assigned to{' '}
{' '}
<span className='font-medium'>{approval.currentStep?.roleName ?? 'another role'}</span>.
</div>
) : null}
{!isPending ? (
<div className='text-muted-foreground text-sm'>
This request is already {approval.request.status.replaceAll('_', ' ')}.
{approval.request.status.replaceAll('_', ' ')}
</div>
) : null}
{isActing ? (
<div className='text-muted-foreground text-xs'>Saving approval action...</div>
<div className='text-muted-foreground text-xs'>...</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Timeline</CardTitle>
<CardDescription>Full audit trail of request submission and approval actions.</CardDescription>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{!approval.timeline.length ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No approval actions recorded yet.
</div>
) : (
approval.timeline.map((item, index) => (
@@ -272,7 +272,7 @@ export function ApprovalRequestPanel({
{item.actorName ?? item.actedBy}
</span>
<span className='text-muted-foreground text-xs'>
Step {item.stepNumber}
{item.stepNumber}
</span>
</div>
<div className='text-muted-foreground text-xs'>

View File

@@ -5,6 +5,13 @@ import { Icons } from '@/components/icons';
export function ApprovalStatusBadge({ status }: { status: string }) {
const normalized = status.toLowerCase();
const labelMap: Record<string, string> = {
pending: 'รออนุมัติ',
approved: 'อนุมัติแล้ว',
rejected: 'ไม่อนุมัติ',
returned: 'ตีกลับ',
cancelled: 'ยกเลิก'
};
const variant =
normalized === 'approved'
? 'default'
@@ -25,7 +32,7 @@ export function ApprovalStatusBadge({ status }: { status: string }) {
return (
<Badge variant={variant} className='capitalize'>
<Icon className='h-3 w-3' />
{status.replaceAll('_', ' ')}
{labelMap[normalized] ?? status.replaceAll('_', ' ')}
</Badge>
);
}