thai-uxui
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -268,7 +268,7 @@ export function QuotationFormSheet({
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label='Billing Customer'>
|
||||
<Field label='ลูกค้าผู้รับใบเสนอราคา'>
|
||||
<Select
|
||||
value={state.customerId}
|
||||
onValueChange={(value) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
51
src/features/crm/shared/terminology.ts
Normal file
51
src/features/crm/shared/terminology.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user