thai-uxui
This commit is contained in:
31
docs/business/crm-terminology.md
Normal file
31
docs/business/crm-terminology.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# CRM Terminology Registry
|
||||||
|
|
||||||
|
| Code | English | Thai | Notes |
|
||||||
|
| ---- | ------- | ---- | ----- |
|
||||||
|
| crm | CRM | CRM | Keep as `CRM` |
|
||||||
|
| dashboard | Dashboard | Dashboard | Keep as `Dashboard` |
|
||||||
|
| lead | Lead | ลีด | ข้อมูลลูกค้าเป้าหมายก่อนส่งต่อให้ฝ่ายขาย |
|
||||||
|
| enquiry | Enquiry | โอกาสขาย | โอกาสทางการขายที่มอบหมายให้ฝ่ายขายดำเนินการ |
|
||||||
|
| quotation | Quotation | ใบเสนอราคา | ใช้กับทุกหน้าที่เกี่ยวกับเอกสารเสนอราคา |
|
||||||
|
| approval | Approval | อนุมัติเอกสาร | ใช้กับ workflow และคิวอนุมัติ |
|
||||||
|
| customer | Customer | ลูกค้า | ใช้กับทะเบียนลูกค้าและบทบาทลูกค้า |
|
||||||
|
| contact | Contact | ผู้ติดต่อ | ใช้กับบุคคลติดต่อของลูกค้า |
|
||||||
|
| billing_customer | Billing Customer | ลูกค้าผู้รับใบเสนอราคา | บริษัทที่ใช้สำหรับออกใบเสนอราคาและวางบิล |
|
||||||
|
| project_parties | Project Parties | ผู้เกี่ยวข้องในโครงการ | ใช้กับ customer roles ใน enquiry และ quotation |
|
||||||
|
| project_party.customer | Customer | ลูกค้า | บทบาทในโครงการ |
|
||||||
|
| project_party.billing_customer | Billing Customer | ลูกค้าผู้รับใบเสนอราคา | บทบาทในโครงการ |
|
||||||
|
| project_party.end_customer | End Customer | เจ้าของโครงการ | บทบาทในโครงการ |
|
||||||
|
| project_party.contractor | Contractor | ผู้รับเหมา | บทบาทในโครงการ |
|
||||||
|
| project_party.consultant | Consultant | ที่ปรึกษา | บทบาทในโครงการ |
|
||||||
|
| lead_count | Lead Count | จำนวนลีด | KPI dashboard |
|
||||||
|
| enquiry_count | Enquiry Count | จำนวนโอกาสขาย | KPI dashboard |
|
||||||
|
| won_count | Won Count | จำนวนงานที่ชนะ | KPI dashboard |
|
||||||
|
| lost_count | Lost Count | จำนวนงานที่แพ้ | KPI dashboard |
|
||||||
|
| revenue | Revenue | มูลค่าใบเสนอราคา | KPI / report label |
|
||||||
|
| conversion_rate | Conversion Rate | อัตราการเปลี่ยนสถานะ | KPI / report label |
|
||||||
|
|
||||||
|
## Freeze Policy
|
||||||
|
|
||||||
|
- เอกสารนี้เป็น source of truth ของคำธุรกิจ CRM ฝั่ง UI
|
||||||
|
- การเปลี่ยนคำหลังจากนี้ต้องผ่าน ADR หรือ business review ก่อน
|
||||||
|
- ห้ามใช้เอกสารนี้เพื่อเปลี่ยนชื่อ schema, API route, TypeScript type, หรือ service folder
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Task UX.1: Thai CRM Terminology Standardization
|
||||||
|
|
||||||
|
## Files Added
|
||||||
|
|
||||||
|
- `docs/business/crm-terminology.md`
|
||||||
|
- `docs/implementation/task-ux1-thai-crm-terminology-standardization.md`
|
||||||
|
- `src/features/crm/shared/terminology.ts`
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `src/config/nav-config.ts`
|
||||||
|
- `src/features/crm/shared/project-party.ts`
|
||||||
|
- `src/app/dashboard/crm/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/customers/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/leads/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/leads/[id]/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/enquiries/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/enquiries/[id]/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/quotations/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/approvals/page.tsx`
|
||||||
|
- `src/app/dashboard/crm/approvals/[id]/page.tsx`
|
||||||
|
- `src/features/crm/dashboard/components/*`
|
||||||
|
- `src/app/api/crm/dashboard/export/route.ts`
|
||||||
|
- `src/features/crm/components/project-parties-editor.tsx`
|
||||||
|
- `src/features/crm/customers/components/*`
|
||||||
|
- `src/features/crm/enquiries/components/*`
|
||||||
|
- `src/features/crm/quotations/components/quotation-columns.tsx`
|
||||||
|
- `src/features/crm/quotations/components/quotation-document-preview.tsx`
|
||||||
|
- `src/features/foundation/approval/components/*`
|
||||||
|
|
||||||
|
## Terminology Registry
|
||||||
|
|
||||||
|
- Added `docs/business/crm-terminology.md` as the frozen source of truth for CRM UI business terms.
|
||||||
|
|
||||||
|
## Menu Changes
|
||||||
|
|
||||||
|
- Standardized CRM side menu labels to Thai business terminology while keeping `CRM` and `Dashboard` in English.
|
||||||
|
|
||||||
|
## Dashboard Changes
|
||||||
|
|
||||||
|
- Updated KPI cards, funnel labels, follow-up labels, approval analytics labels, revenue tables, and export labels to the frozen Thai terms.
|
||||||
|
|
||||||
|
## Form Changes
|
||||||
|
|
||||||
|
- Updated business-facing customer, contact, lead, and enquiry form labels and CTA copy without renaming any data fields.
|
||||||
|
|
||||||
|
## PDF Changes
|
||||||
|
|
||||||
|
- Updated quotation preview labels to Thai business terminology without changing template mapping keys.
|
||||||
|
|
||||||
|
## Report Changes
|
||||||
|
|
||||||
|
- Updated CRM dashboard export headers and section labels to Thai business terminology while keeping internal query/filter codes unchanged.
|
||||||
|
|
||||||
|
## Remaining Risks
|
||||||
|
|
||||||
|
- Some technical or admin-facing screens still intentionally remain English because they are out of scope for this task.
|
||||||
|
- Master option values seeded in the database may still be English in storage, so this task relies on UI copy and display-layer terminology rather than persistence changes.
|
||||||
471
plans/task-ux.1.md
Normal file
471
plans/task-ux.1.md
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
# Task UX.1: Thai CRM Terminology Standardization
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
สร้างมาตรฐานคำศัพท์ภาษาไทยสำหรับ ALLA OS CRM
|
||||||
|
|
||||||
|
เพื่อให้:
|
||||||
|
|
||||||
|
* Menu
|
||||||
|
* Form
|
||||||
|
* Detail Page
|
||||||
|
* Dashboard
|
||||||
|
* Report
|
||||||
|
* PDF
|
||||||
|
* Approval Workflow
|
||||||
|
|
||||||
|
ใช้ภาษาธุรกิจชุดเดียวกัน
|
||||||
|
|
||||||
|
หลัง Task นี้
|
||||||
|
|
||||||
|
Terminology ถือว่า Frozen
|
||||||
|
|
||||||
|
การเปลี่ยนคำในอนาคตต้องผ่าน ADR หรือ Business Review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Background
|
||||||
|
|
||||||
|
CRM ได้ผ่าน
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Task D
|
||||||
|
Task E
|
||||||
|
Task F
|
||||||
|
Task G
|
||||||
|
Task H
|
||||||
|
Task I
|
||||||
|
Task J
|
||||||
|
```
|
||||||
|
|
||||||
|
แล้ว
|
||||||
|
|
||||||
|
ปัจจุบันมีการใช้คำผสมกัน:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead
|
||||||
|
Enquiry
|
||||||
|
Opportunity
|
||||||
|
Quotation
|
||||||
|
Customer
|
||||||
|
Project Party
|
||||||
|
Billing Customer
|
||||||
|
```
|
||||||
|
|
||||||
|
ซึ่งอาจสร้างความสับสนให้ผู้ใช้งาน
|
||||||
|
|
||||||
|
โดยเฉพาะ:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Marketing
|
||||||
|
Sales
|
||||||
|
Manager
|
||||||
|
Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.1 Terminology Registry
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
docs/business/crm-terminology.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Document format:
|
||||||
|
|
||||||
|
| Code | English | Thai | Notes |
|
||||||
|
| ---- | ------- | ---- | ----- |
|
||||||
|
|
||||||
|
This file becomes the source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.2 Frozen CRM Business Terms
|
||||||
|
|
||||||
|
## CRM
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
CRM
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not translate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lead
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ลีด
|
||||||
|
```
|
||||||
|
|
||||||
|
Definition:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ข้อมูลลูกค้าเป้าหมายก่อนส่งต่อให้ฝ่ายขาย
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enquiry
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
โอกาสขาย
|
||||||
|
```
|
||||||
|
|
||||||
|
Definition:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
โอกาสทางการขายที่ได้รับมอบหมายให้ฝ่ายขายดำเนินการ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quotation
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ใบเสนอราคา
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Approval
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
อนุมัติเอกสาร
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customer
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ลูกค้า
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ผู้ติดต่อ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Billing Customer
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ลูกค้าผู้รับใบเสนอราคา
|
||||||
|
```
|
||||||
|
|
||||||
|
Definition:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
บริษัทที่ใช้สำหรับออกใบเสนอราคาและวางบิล
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Parties
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ผู้เกี่ยวข้องในโครงการ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.3 Project Party Roles
|
||||||
|
|
||||||
|
## customer
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ลูกค้า
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## billing_customer
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ลูกค้าผู้รับใบเสนอราคา
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## end_customer
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
เจ้าของโครงการ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## contractor
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ผู้รับเหมา
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## consultant
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ที่ปรึกษา
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.4 Dashboard KPI Terms
|
||||||
|
|
||||||
|
## Lead Count
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
จำนวนลีด
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enquiry Count
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
จำนวนโอกาสขาย
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Won Count
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
จำนวนงานที่ชนะ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lost Count
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
จำนวนงานที่แพ้
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Revenue
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
มูลค่าใบเสนอราคา
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conversion Rate
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
อัตราการเปลี่ยนสถานะ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.5 Status Labels
|
||||||
|
|
||||||
|
## Lead
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ใหม่
|
||||||
|
ติดต่อแล้ว
|
||||||
|
ผ่านการคัดกรอง
|
||||||
|
ไม่ผ่านการคัดกรอง
|
||||||
|
ส่งต่อฝ่ายขายแล้ว
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enquiry
|
||||||
|
|
||||||
|
```txt
|
||||||
|
โอกาสขาย
|
||||||
|
ชนะ
|
||||||
|
แพ้
|
||||||
|
```
|
||||||
|
|
||||||
|
Mapped from:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
lead
|
||||||
|
enquiry
|
||||||
|
closed_won
|
||||||
|
closed_lost
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.6 Menu Labels
|
||||||
|
|
||||||
|
CRM Menu:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Dashboard
|
||||||
|
ลีด
|
||||||
|
โอกาสขาย
|
||||||
|
ใบเสนอราคา
|
||||||
|
อนุมัติเอกสาร
|
||||||
|
ตั้งค่า CRM
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.7 Form Labels
|
||||||
|
|
||||||
|
Update labels only.
|
||||||
|
|
||||||
|
Do NOT rename:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
database fields
|
||||||
|
api fields
|
||||||
|
typescript types
|
||||||
|
server service names
|
||||||
|
table names
|
||||||
|
```
|
||||||
|
|
||||||
|
Business label only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.8 PDF Terminology
|
||||||
|
|
||||||
|
Update document preview and PDF rendering labels.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Billing Customer
|
||||||
|
→ ลูกค้าผู้รับใบเสนอราคา
|
||||||
|
|
||||||
|
Project Parties
|
||||||
|
→ ผู้เกี่ยวข้องในโครงการ
|
||||||
|
|
||||||
|
Quotation
|
||||||
|
→ ใบเสนอราคา
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not modify template mapping keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope UX.1.9 Reports
|
||||||
|
|
||||||
|
Report headers must use:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Thai business terms
|
||||||
|
```
|
||||||
|
|
||||||
|
while keeping export column codes unchanged internally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Explicit Non-Scope
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Rename database schema
|
||||||
|
Rename API routes
|
||||||
|
Rename TS types
|
||||||
|
Rename service folders
|
||||||
|
Create i18n system
|
||||||
|
Translate admin technical screens
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Output
|
||||||
|
|
||||||
|
1. Files Added
|
||||||
|
2. Files Modified
|
||||||
|
3. Terminology Registry
|
||||||
|
4. Menu Changes
|
||||||
|
5. Dashboard Changes
|
||||||
|
6. Form Changes
|
||||||
|
7. PDF Changes
|
||||||
|
8. Report Changes
|
||||||
|
9. Remaining Risks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Definition of Done
|
||||||
|
|
||||||
|
✔ CRM terminology registry exists
|
||||||
|
|
||||||
|
✔ Menu terminology standardized
|
||||||
|
|
||||||
|
✔ Lead terminology standardized
|
||||||
|
|
||||||
|
✔ Enquiry terminology standardized
|
||||||
|
|
||||||
|
✔ Project Party terminology standardized
|
||||||
|
|
||||||
|
✔ Dashboard terminology standardized
|
||||||
|
|
||||||
|
✔ PDF terminology standardized
|
||||||
|
|
||||||
|
✔ Report terminology standardized
|
||||||
|
|
||||||
|
✔ No database changes
|
||||||
|
|
||||||
|
✔ No API changes
|
||||||
|
|
||||||
|
✔ No schema rename
|
||||||
@@ -56,12 +56,12 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
if (section === 'revenue-analytics') {
|
if (section === 'revenue-analytics') {
|
||||||
rows = [
|
rows = [
|
||||||
['Dimension', 'Customer Code', 'Customer Name', 'Revenue', 'Quotation Count', 'Won Revenue'],
|
['มิติรายงาน', 'รหัสลูกค้า', 'ชื่อลูกค้า', 'มูลค่าใบเสนอราคา', 'จำนวนใบเสนอราคา', 'มูลค่างานที่ชนะ'],
|
||||||
...[
|
...[
|
||||||
['End Customer', ...data.revenueAnalytics.topEndCustomers],
|
['เจ้าของโครงการ', ...data.revenueAnalytics.topEndCustomers],
|
||||||
['Billing Customer', ...data.revenueAnalytics.topBillingCustomers],
|
['ลูกค้าผู้รับใบเสนอราคา', ...data.revenueAnalytics.topBillingCustomers],
|
||||||
['Contractor', ...data.revenueAnalytics.topContractors],
|
['ผู้รับเหมา', ...data.revenueAnalytics.topContractors],
|
||||||
['Consultant', ...data.revenueAnalytics.topConsultants]
|
['ที่ปรึกษา', ...data.revenueAnalytics.topConsultants]
|
||||||
].flatMap(([dimension, ...entries]) =>
|
].flatMap(([dimension, ...entries]) =>
|
||||||
(entries as typeof data.revenueAnalytics.topEndCustomers).map((row) => [
|
(entries as typeof data.revenueAnalytics.topEndCustomers).map((row) => [
|
||||||
String(dimension),
|
String(dimension),
|
||||||
@@ -75,7 +75,7 @@ export async function GET(request: NextRequest) {
|
|||||||
];
|
];
|
||||||
} else if (section === 'pipeline-funnel') {
|
} else if (section === 'pipeline-funnel') {
|
||||||
rows = [
|
rows = [
|
||||||
['Stage', 'Count', 'Value', 'Conversion Rate'],
|
['ขั้นตอน', 'จำนวน', 'มูลค่า', 'อัตราการเปลี่ยนสถานะ'],
|
||||||
...data.funnel.map((row) => [
|
...data.funnel.map((row) => [
|
||||||
row.label,
|
row.label,
|
||||||
String(row.count),
|
String(row.count),
|
||||||
@@ -85,7 +85,7 @@ export async function GET(request: NextRequest) {
|
|||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
rows = [
|
rows = [
|
||||||
['Sales Person', 'Lead Count', 'Enquiry Count', 'Quotation Count', 'Approved Quotations', 'Won Revenue', 'Conversion Rate'],
|
['ฝ่ายขาย', 'จำนวนลีด', 'จำนวนโอกาสขาย', 'จำนวนใบเสนอราคา', 'จำนวนใบเสนอราคาที่อนุมัติแล้ว', 'มูลค่างานที่ชนะ', 'อัตราการเปลี่ยนสถานะ'],
|
||||||
...data.salesRanking.map((row) => [
|
...data.salesRanking.map((row) => [
|
||||||
row.salesPersonName,
|
row.salesPersonName,
|
||||||
String(row.leadCount),
|
String(row.leadCount),
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default async function ApprovalDetailRoute({ params }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Approval Detail'
|
pageTitle='รายละเอียดการอนุมัติเอกสาร'
|
||||||
pageDescription='Workflow steps, current approver, and timeline for a production approval request.'
|
pageDescription='ลำดับขั้น ผู้อนุมัติปัจจุบัน และไทม์ไลน์ของคำขออนุมัติเอกสาร'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export default async function ApprovalsRoute(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Approvals'
|
pageTitle='อนุมัติเอกสาร'
|
||||||
pageDescription='Production approval queue for quotations, with real workflow routing and action history.'
|
pageDescription='คิวงานอนุมัติเอกสารสำหรับใบเสนอราคา พร้อมลำดับอนุมัติและประวัติการดำเนินการ'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default async function CustomersRoute(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Customers'
|
pageTitle='ลูกค้า'
|
||||||
pageDescription='Production customer registry with organization scoping, branch filters, contact management, and audit-backed history.'
|
pageDescription='ทะเบียนลูกค้า พร้อมข้อมูลผู้ติดต่อ สาขาที่ดูแล และประวัติการใช้งานใน CRM'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ export default async function EnquiryDetailRoute({ params }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Enquiry Detail'
|
pageTitle='รายละเอียดโอกาสขาย'
|
||||||
pageDescription='Lead or enquiry context, follow-up timeline, and ownership lifecycle.'
|
pageDescription='ข้อมูลโอกาสขาย ไทม์ไลน์การติดตาม และสถานะการดูแลของฝ่ายขาย'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export default async function EnquiriesRoute(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle="Enquiries"
|
pageTitle="โอกาสขาย"
|
||||||
pageDescription="Sales workspace for active enquiries, follow-up workflow, and quotation preparation."
|
pageDescription="พื้นที่ทำงานของฝ่ายขายสำหรับดูแลโอกาสขาย ติดตามความคืบหน้า และเตรียมใบเสนอราคา"
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className="text-muted-foreground rounded-lg border border-dashed p-8 text-center">
|
<div className="text-muted-foreground rounded-lg border border-dashed p-8 text-center">
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ export default async function LeadDetailRoute({ params }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Lead Detail'
|
pageTitle='รายละเอียดลีด'
|
||||||
pageDescription='Marketing lead information and assignment handoff context.'
|
pageDescription='ข้อมูลลีดสำหรับการคัดกรอง ติดตาม และส่งต่อให้ฝ่ายขาย'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export default async function LeadsRoute(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Leads'
|
pageTitle='ลีด'
|
||||||
pageDescription='Marketing workspace for lead collection, qualification, nurturing, and sales assignment.'
|
pageDescription='พื้นที่ทำงานของการตลาดสำหรับเก็บลีด คัดกรองความพร้อม และส่งต่อให้ฝ่ายขาย'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default async function CrmDashboardRoute(props: PageProps) {
|
|||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='CRM Dashboard'
|
pageTitle='CRM Dashboard'
|
||||||
pageDescription='Production KPI dashboard, revenue analytics, approval visibility, and sales insights from real CRM data.'
|
pageDescription='Dashboard กลางสำหรับติดตาม KPI, มูลค่าใบเสนอราคา, งานอนุมัติเอกสาร, และภาพรวมการขายจากข้อมูล CRM จริง'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ export default async function QuotationDetailRoute({ params }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Quotation Detail'
|
pageTitle='รายละเอียดใบเสนอราคา'
|
||||||
pageDescription='Commercial detail, child records, revisions, and audit activity.'
|
pageDescription='ข้อมูลใบเสนอราคา รายการย่อย revision และประวัติการดำเนินการที่เกี่ยวข้อง'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default async function QuotationsRoute(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
pageTitle='Quotations'
|
pageTitle='ใบเสนอราคา'
|
||||||
pageDescription='Production quotation register with revisions, related parties, and server-calculated items.'
|
pageDescription='ทะเบียนใบเสนอราคา พร้อม revision, ผู้เกี่ยวข้องในโครงการ, และรายการคำนวณจากระบบ'
|
||||||
access={canRead}
|
access={canRead}
|
||||||
accessFallback={
|
accessFallback={
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
label: "CRM",
|
label: "CRM",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Dashboard CRM",
|
title: "Dashboard",
|
||||||
url: "/dashboard/crm",
|
url: "/dashboard/crm",
|
||||||
icon: "dashboard",
|
icon: "dashboard",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -92,7 +92,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, permission: "crm.dashboard.read" },
|
access: { requireOrg: true, permission: "crm.dashboard.read" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Customers",
|
title: "ลูกค้า",
|
||||||
url: "/dashboard/crm/customers",
|
url: "/dashboard/crm/customers",
|
||||||
icon: "teams",
|
icon: "teams",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -100,7 +100,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, permission: "crm.customer.read" },
|
access: { requireOrg: true, permission: "crm.customer.read" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Leads",
|
title: "ลีด",
|
||||||
url: "/dashboard/crm/leads",
|
url: "/dashboard/crm/leads",
|
||||||
icon: "forms",
|
icon: "forms",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -108,7 +108,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, permission: "crm.lead.read" },
|
access: { requireOrg: true, permission: "crm.lead.read" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Enquiries",
|
title: "โอกาสขาย",
|
||||||
url: "/dashboard/crm/enquiries",
|
url: "/dashboard/crm/enquiries",
|
||||||
icon: "forms",
|
icon: "forms",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -116,7 +116,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, permission: "crm.enquiry.read" },
|
access: { requireOrg: true, permission: "crm.enquiry.read" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quotations",
|
title: "ใบเสนอราคา",
|
||||||
url: "/dashboard/crm/quotations",
|
url: "/dashboard/crm/quotations",
|
||||||
icon: "post",
|
icon: "post",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -124,7 +124,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, role: "admin" },
|
access: { requireOrg: true, role: "admin" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Approvals",
|
title: "อนุมัติเอกสาร",
|
||||||
url: "/dashboard/crm/approvals",
|
url: "/dashboard/crm/approvals",
|
||||||
icon: "checks",
|
icon: "checks",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -132,7 +132,7 @@ export const navGroups: NavGroup[] = [
|
|||||||
access: { requireOrg: true, permission: "crm.approval.read" },
|
access: { requireOrg: true, permission: "crm.approval.read" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "CRM Settings",
|
title: "ตั้งค่า CRM",
|
||||||
url: "/dashboard/crm/settings/master-options",
|
url: "/dashboard/crm/settings/master-options",
|
||||||
icon: "settings",
|
icon: "settings",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
|
|||||||
@@ -75,19 +75,19 @@ export function ProjectPartiesEditor({
|
|||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<div className='text-sm font-medium'>Project Parties</div>
|
<div className='text-sm font-medium'>ผู้เกี่ยวข้องในโครงการ</div>
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
Add related companies with their role and optional remark.
|
เพิ่มรายชื่อบริษัทที่เกี่ยวข้อง พร้อมบทบาทและหมายเหตุเพิ่มเติม
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type='button' variant='outline' size='sm' onClick={addItem}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!items.length ? (
|
{!items.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -95,14 +95,14 @@ export function ProjectPartiesEditor({
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={item.key} className='rounded-lg border p-4'>
|
<div key={item.key} className='rounded-lg border p-4'>
|
||||||
<div className='mb-3 flex items-center justify-between gap-3'>
|
<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
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => removeItem(item.key)}
|
onClick={() => removeItem(item.key)}
|
||||||
>
|
>
|
||||||
<Icons.trash className='mr-2 h-4 w-4' /> Remove
|
<Icons.trash className='mr-2 h-4 w-4' /> ลบ
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid gap-3 md:grid-cols-3'>
|
<div className='grid gap-3 md:grid-cols-3'>
|
||||||
@@ -113,10 +113,10 @@ export function ProjectPartiesEditor({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Customer' />
|
<SelectValue placeholder='ลูกค้า' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__none__'>Select customer</SelectItem>
|
<SelectItem value='__none__'>เลือกลูกค้า</SelectItem>
|
||||||
{customers.map((customer) => (
|
{customers.map((customer) => (
|
||||||
<SelectItem key={customer.id} value={customer.id}>
|
<SelectItem key={customer.id} value={customer.id}>
|
||||||
{customer.name} ({customer.code})
|
{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)}>
|
<Select value={item.role || '__none__'} onValueChange={(value) => updateItem(item.key, 'role', value === '__none__' ? '' : value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Role' />
|
<SelectValue placeholder='บทบาท' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__none__'>Select role</SelectItem>
|
<SelectItem value='__none__'>เลือกบทบาท</SelectItem>
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<SelectItem key={role.id} value={role.id}>
|
<SelectItem key={role.id} value={role.id}>
|
||||||
{role.label}
|
{role.label}
|
||||||
@@ -142,7 +142,7 @@ export function ProjectPartiesEditor({
|
|||||||
<Input
|
<Input
|
||||||
value={item.remark}
|
value={item.remark}
|
||||||
onChange={(event) => updateItem(item.key, 'remark', event.target.value)}
|
onChange={(event) => updateItem(item.key, 'remark', event.target.value)}
|
||||||
placeholder='Remark'
|
placeholder='หมายเหตุ'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,21 +51,21 @@ export function ContactFormSheet({
|
|||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
...createCustomerContactMutation,
|
...createCustomerContactMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Contact created successfully');
|
toast.success('สร้างผู้ติดต่อสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to create contact')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถสร้างผู้ติดต่อได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
...updateCustomerContactMutation,
|
...updateCustomerContactMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Contact updated successfully');
|
toast.success('อัปเดตผู้ติดต่อสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to update contact')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอัปเดตผู้ติดต่อได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
@@ -106,43 +106,41 @@ export function ContactFormSheet({
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className='flex flex-col sm:max-w-2xl'>
|
<SheetContent className='flex flex-col sm:max-w-2xl'>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{isEdit ? 'Edit Contact' : 'New Contact'}</SheetTitle>
|
<SheetTitle>{isEdit ? 'แก้ไขผู้ติดต่อ' : 'เพิ่มผู้ติดต่อ'}</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>บันทึกข้อมูลผู้ติดต่อของลูกค้ารายนี้</SheetDescription>
|
||||||
Capture the operational contact person for this customer account.
|
|
||||||
</SheetDescription>
|
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className='flex-1 overflow-auto'>
|
<div className='flex-1 overflow-auto'>
|
||||||
<form.AppForm>
|
<form.AppForm>
|
||||||
<form.Form id='customer-contact-form' className='grid gap-4 md:grid-cols-2'>
|
<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='name' label='ชื่อผู้ติดต่อ' required placeholder='Jane Doe' />
|
||||||
<FormTextField name='position' label='Position' placeholder='Procurement Manager' />
|
<FormTextField name='position' label='ตำแหน่ง' placeholder='Procurement Manager' />
|
||||||
<FormTextField name='department' label='Department' placeholder='Procurement' />
|
<FormTextField name='department' label='แผนก' placeholder='Procurement' />
|
||||||
<FormTextField name='phone' label='Phone' placeholder='02-000-0000' />
|
<FormTextField name='phone' label='โทรศัพท์' placeholder='02-000-0000' />
|
||||||
<FormTextField name='mobile' label='Mobile' placeholder='081-000-0000' />
|
<FormTextField name='mobile' label='มือถือ' placeholder='081-000-0000' />
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='email'
|
name='email'
|
||||||
label='Email'
|
label='อีเมล'
|
||||||
type='email'
|
type='email'
|
||||||
placeholder='jane@example.com'
|
placeholder='jane@example.com'
|
||||||
/>
|
/>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='notes'
|
name='notes'
|
||||||
label='Notes'
|
label='หมายเหตุ'
|
||||||
placeholder='Preferred communication style, escalation notes, or meeting context'
|
placeholder='รูปแบบการติดต่อที่เหมาะสม หมายเหตุการประสานงาน หรือบริบทการประชุม'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
||||||
<FormSwitchField
|
<FormSwitchField
|
||||||
name='isPrimary'
|
name='isPrimary'
|
||||||
label='Primary Contact'
|
label='ผู้ติดต่อหลัก'
|
||||||
description='When enabled, this contact becomes the main person for this customer.'
|
description='เมื่อเปิดใช้งาน ผู้ติดต่อนี้จะเป็นผู้ติดต่อหลักของลูกค้ารายนี้'
|
||||||
/>
|
/>
|
||||||
<FormSwitchField
|
<FormSwitchField
|
||||||
name='isActive'
|
name='isActive'
|
||||||
label='Active Contact'
|
label='ใช้งานอยู่'
|
||||||
description='Inactive contacts remain in history and can be restored later.'
|
description='ผู้ติดต่อที่ปิดใช้งานยังคงอยู่ในประวัติและเปิดกลับมาใช้ได้ภายหลัง'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form.Form>
|
</form.Form>
|
||||||
@@ -151,11 +149,11 @@ export function ContactFormSheet({
|
|||||||
|
|
||||||
<SheetFooter>
|
<SheetFooter>
|
||||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
ยกเลิก
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' form='customer-contact-form' isLoading={isPending}>
|
<Button type='submit' form='customer-contact-form' isLoading={isPending}>
|
||||||
<Icons.check className='mr-2 h-4 w-4' />
|
<Icons.check className='mr-2 h-4 w-4' />
|
||||||
{isEdit ? 'Update Contact' : 'Create Contact'}
|
{isEdit ? 'อัปเดตผู้ติดต่อ' : 'สร้างผู้ติดต่อ'}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ export function CustomerCellAction({
|
|||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteCustomerMutation,
|
...deleteCustomerMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Customer deleted successfully');
|
toast.success('ลบลูกค้าสำเร็จ');
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to delete customer')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบลูกค้าได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,15 +65,15 @@ export function CustomerCellAction({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>การดำเนินการ</DropdownMenuLabel>
|
||||||
<DropdownMenuItem onClick={() => router.push(`/dashboard/crm/customers/${data.id}`)}>
|
<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>
|
||||||
<DropdownMenuItem onClick={() => setEditOpen(true)} disabled={!canUpdate}>
|
<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>
|
||||||
<DropdownMenuItem onClick={() => setDeleteOpen(true)} disabled={!canDelete}>
|
<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>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function getCustomerColumns({
|
|||||||
id: 'name',
|
id: 'name',
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
|
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Customer' />
|
<DataTableColumnHeader column={column} title='ลูกค้า' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
@@ -46,8 +46,8 @@ export function getCustomerColumns({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Customer',
|
label: 'ลูกค้า',
|
||||||
placeholder: 'Search customers...',
|
placeholder: 'ค้นหาลูกค้า...',
|
||||||
variant: 'text' as const,
|
variant: 'text' as const,
|
||||||
icon: Icons.search
|
icon: Icons.search
|
||||||
},
|
},
|
||||||
@@ -85,7 +85,7 @@ export function getCustomerColumns({
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Type',
|
label: 'ประเภท',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.customerTypes.map((item) => ({
|
options: referenceData.customerTypes.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -108,7 +108,7 @@ export function getCustomerColumns({
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Branch',
|
label: 'สาขา',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.branches.map((item) => ({
|
options: referenceData.branches.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -137,7 +137,7 @@ export function getCustomerColumns({
|
|||||||
id: 'contactCount',
|
id: 'contactCount',
|
||||||
accessorKey: 'contactCount',
|
accessorKey: 'contactCount',
|
||||||
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
|
header: ({ column }: { column: Column<CustomerListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Contacts' />
|
<DataTableColumnHeader column={column} title='ผู้ติดต่อ' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => <span>{row.original.contactCount}</span>
|
cell: ({ row }) => <span>{row.original.contactCount}</span>
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,20 +32,20 @@ export function CustomerContactsTab({
|
|||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteCustomerContactMutation,
|
...deleteCustomerContactMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Contact deleted successfully');
|
toast.success('ลบผู้ติดต่อสำเร็จ');
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to delete contact')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบผู้ติดต่อได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Contacts</CardTitle>
|
<CardTitle>ผู้ติดต่อ</CardTitle>
|
||||||
<CardDescription>Manage the people attached to this customer account.</CardDescription>
|
<CardDescription>จัดการรายชื่อบุคคลที่ติดต่อกับลูกค้ารายนี้</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{canCreate ? (
|
{canCreate ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -55,14 +55,14 @@ export function CustomerContactsTab({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.add className='mr-2 h-4 w-4' /> Add Contact
|
<Icons.add className='mr-2 h-4 w-4' /> เพิ่มผู้ติดต่อ
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{!data.items.length ? (
|
{!data.items.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||||
No contacts have been added yet.
|
ยังไม่มีผู้ติดต่อ
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.items.map((contact) => (
|
data.items.map((contact) => (
|
||||||
@@ -73,16 +73,16 @@ export function CustomerContactsTab({
|
|||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<span className='font-medium'>{contact.name}</span>
|
<span className='font-medium'>{contact.name}</span>
|
||||||
{contact.isPrimary ? <Badge>Primary</Badge> : null}
|
{contact.isPrimary ? <Badge>ผู้ติดต่อหลัก</Badge> : null}
|
||||||
{!contact.isActive ? <Badge variant='outline'>Inactive</Badge> : null}
|
{!contact.isActive ? <Badge variant='outline'>ปิดใช้งาน</Badge> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{[contact.position, contact.department].filter(Boolean).join(' • ') ||
|
{[contact.position, contact.department].filter(Boolean).join(' • ') ||
|
||||||
'No role details'}
|
'ยังไม่มีข้อมูลตำแหน่ง'}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{[contact.phone, contact.mobile, contact.email].filter(Boolean).join(' • ') ||
|
{[contact.phone, contact.mobile, contact.email].filter(Boolean).join(' • ') ||
|
||||||
'No contact methods'}
|
'ยังไม่มีช่องทางติดต่อ'}
|
||||||
</div>
|
</div>
|
||||||
{contact.notes ? <p className='text-sm'>{contact.notes}</p> : null}
|
{contact.notes ? <p className='text-sm'>{contact.notes}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@ export function CustomerContactsTab({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.edit className='mr-2 h-4 w-4' /> Edit
|
<Icons.edit className='mr-2 h-4 w-4' /> แก้ไข
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -107,7 +107,7 @@ export function CustomerContactsTab({
|
|||||||
setDeleteOpen(true);
|
setDeleteOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.trash className='mr-2 h-4 w-4' /> Delete
|
<Icons.trash className='mr-2 h-4 w-4' /> ลบ
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,14 +100,14 @@ export function CustomerDetail({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-semibold'>{customer.name}</h2>
|
<h2 className='text-2xl font-semibold'>{customer.name}</h2>
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
{customer.abbr || customer.email || customer.phone || 'Customer detail record'}
|
{customer.abbr || customer.email || customer.phone || 'รายละเอียดข้อมูลลูกค้า'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{canUpdate ? (
|
{canUpdate ? (
|
||||||
<Button variant='outline' onClick={() => setEditOpen(true)}>
|
<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>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -119,18 +119,16 @@ export function CustomerDetail({
|
|||||||
<CardContent className='pt-6'>
|
<CardContent className='pt-6'>
|
||||||
<Tabs defaultValue='overview' className='gap-4'>
|
<Tabs defaultValue='overview' className='gap-4'>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='overview'>Overview</TabsTrigger>
|
<TabsTrigger value='overview'>ภาพรวม</TabsTrigger>
|
||||||
{canReadContacts ? <TabsTrigger value='contacts'>Contacts</TabsTrigger> : null}
|
{canReadContacts ? <TabsTrigger value='contacts'>ผู้ติดต่อ</TabsTrigger> : null}
|
||||||
<TabsTrigger value='activity'>Activity</TabsTrigger>
|
<TabsTrigger value='activity'>กิจกรรม</TabsTrigger>
|
||||||
<TabsTrigger value='related'>Related Documents</TabsTrigger>
|
<TabsTrigger value='related'>เอกสารที่เกี่ยวข้อง</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='overview'>
|
<TabsContent value='overview'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Overview</CardTitle>
|
<CardTitle>ข้อมูลลูกค้า</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>ข้อมูลหลักของลูกค้า ที่อยู่ และการจัดประเภทใน CRM</CardDescription>
|
||||||
Core account data, location, and CRM classification.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
<FieldItem label='Tax ID' value={customer.taxId} />
|
<FieldItem label='Tax ID' value={customer.taxId} />
|
||||||
@@ -142,21 +140,21 @@ export function CustomerDetail({
|
|||||||
value={customer.branchId ? branchMap.get(customer.branchId) : null}
|
value={customer.branchId ? branchMap.get(customer.branchId) : null}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Lead Channel'
|
label='ที่มาของลีด'
|
||||||
value={leadMap.get(customer.leadChannel ?? '')}
|
value={leadMap.get(customer.leadChannel ?? '')}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Customer Group'
|
label='กลุ่มลูกค้า'
|
||||||
value={groupMap.get(customer.customerGroup ?? '')}
|
value={groupMap.get(customer.customerGroup ?? '')}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Customer Sub Group'
|
label='กลุ่มย่อยลูกค้า'
|
||||||
value={subGroupMap.get(customer.customerSubGroup ?? '')}
|
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'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Address'
|
label='ที่อยู่'
|
||||||
value={
|
value={
|
||||||
[
|
[
|
||||||
customer.address,
|
customer.address,
|
||||||
@@ -172,7 +170,7 @@ export function CustomerDetail({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 xl:col-span-4'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<FieldItem label='Notes' value={customer.notes} />
|
<FieldItem label='หมายเหตุ' value={customer.notes} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -190,15 +188,13 @@ export function CustomerDetail({
|
|||||||
<TabsContent value='activity'>
|
<TabsContent value='activity'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Activity</CardTitle>
|
<CardTitle>กิจกรรม</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>ประวัติการเปลี่ยนแปลงที่เกิดจากการแก้ไขข้อมูลลูกค้า</CardDescription>
|
||||||
Audit log entries captured from customer mutations.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{!data.activity.length ? (
|
{!data.activity.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||||
No activity recorded yet.
|
ยังไม่มีกิจกรรม
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.activity.map((item, index) => (
|
data.activity.map((item, index) => (
|
||||||
@@ -229,15 +225,13 @@ export function CustomerDetail({
|
|||||||
<TabsContent value='related'>
|
<TabsContent value='related'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Related Documents</CardTitle>
|
<CardTitle>เอกสารที่เกี่ยวข้อง</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>ลิงก์ไปยังโอกาสขายและใบเสนอราคาที่เกี่ยวข้องกับลูกค้ารายนี้</CardDescription>
|
||||||
Production enquiry and quotation links for this customer.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!relatedEnquiries.length && !relatedQuotations.length ? (
|
{!relatedEnquiries.length && !relatedQuotations.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
@@ -288,8 +282,8 @@ export function CustomerDetail({
|
|||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Record Snapshot</CardTitle>
|
<CardTitle>ข้อมูลระบบ</CardTitle>
|
||||||
<CardDescription>Operational metadata for this customer row.</CardDescription>
|
<CardDescription>ข้อมูลประกอบการติดตามและตรวจสอบของรายการลูกค้านี้</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
<FieldItem label='Created By' value={customer.createdBy} />
|
<FieldItem label='Created By' value={customer.createdBy} />
|
||||||
|
|||||||
@@ -73,21 +73,21 @@ export function CustomerFormSheet({
|
|||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
...createCustomerMutation,
|
...createCustomerMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Customer created successfully');
|
toast.success('สร้างลูกค้าสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to create customer')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถสร้างลูกค้าได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
...updateCustomerMutation,
|
...updateCustomerMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Customer updated successfully');
|
toast.success('อัปเดตลูกค้าสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to update customer')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอัปเดตลูกค้าได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
@@ -146,10 +146,8 @@ export function CustomerFormSheet({
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className='flex flex-col sm:max-w-3xl'>
|
<SheetContent className='flex flex-col sm:max-w-3xl'>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{isEdit ? 'Edit Customer' : 'New Customer'}</SheetTitle>
|
<SheetTitle>{isEdit ? 'แก้ไขลูกค้า' : 'เพิ่มลูกค้า'}</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>จัดการข้อมูลหลักของลูกค้า สาขาที่ดูแล และการจัดประเภทใน CRM</SheetDescription>
|
||||||
Maintain the core account profile, ownership branch, and CRM classification here.
|
|
||||||
</SheetDescription>
|
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className='flex-1 overflow-auto'>
|
<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'>
|
<form.Form id='customer-form-sheet' className='grid gap-4 md:grid-cols-2'>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='name'
|
name='name'
|
||||||
label='Customer Name'
|
label='ชื่อลูกค้า'
|
||||||
required
|
required
|
||||||
placeholder='ALLA Service Co., Ltd.'
|
placeholder='ALLA Service Co., Ltd.'
|
||||||
/>
|
/>
|
||||||
<FormTextField name='abbr' label='Abbreviation' placeholder='ALLA' />
|
<FormTextField name='abbr' label='ชื่อย่อ' placeholder='ALLA' />
|
||||||
<FormTextField name='taxId' label='Tax ID' placeholder='0105556...' />
|
<FormTextField name='taxId' label='เลขประจำตัวผู้เสียภาษี' placeholder='0105556...' />
|
||||||
<FormTextField name='phone' label='Phone' placeholder='02-000-0000' />
|
<FormTextField name='phone' label='โทรศัพท์' placeholder='02-000-0000' />
|
||||||
<FormTextField name='fax' label='Fax' placeholder='02-000-0001' />
|
<FormTextField name='fax' label='แฟกซ์' placeholder='02-000-0001' />
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='email'
|
name='email'
|
||||||
label='Email'
|
label='อีเมล'
|
||||||
type='email'
|
type='email'
|
||||||
placeholder='sales@example.com'
|
placeholder='sales@example.com'
|
||||||
/>
|
/>
|
||||||
<FormTextField name='website' label='Website' placeholder='https://example.com' />
|
<FormTextField name='website' label='เว็บไซต์' placeholder='https://example.com' />
|
||||||
<FormTextField name='country' label='Country' placeholder='Thailand' />
|
<FormTextField name='country' label='ประเทศ' placeholder='Thailand' />
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='customerType'
|
name='customerType'
|
||||||
label='Customer Type'
|
label='ประเภทลูกค้า'
|
||||||
required
|
required
|
||||||
options={referenceData.customerTypes.map((item) => ({
|
options={referenceData.customerTypes.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -184,7 +182,7 @@ export function CustomerFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='customerStatus'
|
name='customerStatus'
|
||||||
label='Customer Status'
|
label='สถานะลูกค้า'
|
||||||
required
|
required
|
||||||
options={referenceData.customerStatuses.map((item) => ({
|
options={referenceData.customerStatuses.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -193,7 +191,7 @@ export function CustomerFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='branchId'
|
name='branchId'
|
||||||
label='Branch'
|
label='สาขา'
|
||||||
options={referenceData.branches.map((branch) => ({
|
options={referenceData.branches.map((branch) => ({
|
||||||
value: branch.id,
|
value: branch.id,
|
||||||
label: branch.name
|
label: branch.name
|
||||||
@@ -201,7 +199,7 @@ export function CustomerFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='leadChannel'
|
name='leadChannel'
|
||||||
label='Lead Channel'
|
label='ที่มาของลีด'
|
||||||
options={referenceData.leadChannels.map((item) => ({
|
options={referenceData.leadChannels.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.label
|
label: item.label
|
||||||
@@ -209,7 +207,7 @@ export function CustomerFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='customerGroup'
|
name='customerGroup'
|
||||||
label='Customer Group'
|
label='กลุ่มลูกค้า'
|
||||||
options={referenceData.customerGroups.map((item) => ({
|
options={referenceData.customerGroups.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.label
|
label: item.label
|
||||||
@@ -217,10 +215,10 @@ export function CustomerFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='customerSubGroup'
|
name='customerSubGroup'
|
||||||
label='Customer Sub Group'
|
label='กลุ่มย่อยลูกค้า'
|
||||||
disabled={!selectedCustomerGroup}
|
disabled={!selectedCustomerGroup}
|
||||||
placeholder={
|
placeholder={
|
||||||
selectedCustomerGroup ? 'Select customer sub group' : 'Select customer group first'
|
selectedCustomerGroup ? 'เลือกกลุ่มย่อยลูกค้า' : 'เลือกกลุ่มลูกค้าก่อน'
|
||||||
}
|
}
|
||||||
options={filteredCustomerSubGroups.map((item) => ({
|
options={filteredCustomerSubGroups.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -228,24 +226,24 @@ export function CustomerFormSheet({
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextField name='address' label='Address' placeholder='123 Main Road' />
|
<FormTextField name='address' label='ที่อยู่' placeholder='123 Main Road' />
|
||||||
</div>
|
</div>
|
||||||
<FormTextField name='province' label='Province' placeholder='Bangkok' />
|
<FormTextField name='province' label='จังหวัด' placeholder='Bangkok' />
|
||||||
<FormTextField name='district' label='District' placeholder='Bang Kapi' />
|
<FormTextField name='district' label='เขต / อำเภอ' placeholder='Bang Kapi' />
|
||||||
<FormTextField name='subDistrict' label='Sub District' placeholder='Hua Mak' />
|
<FormTextField name='subDistrict' label='แขวง / ตำบล' placeholder='Hua Mak' />
|
||||||
<FormTextField name='postalCode' label='Postal Code' placeholder='10240' />
|
<FormTextField name='postalCode' label='รหัสไปรษณีย์' placeholder='10240' />
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='notes'
|
name='notes'
|
||||||
label='Notes'
|
label='หมายเหตุ'
|
||||||
placeholder='Internal notes about this customer relationship'
|
placeholder='หมายเหตุภายในเกี่ยวกับความสัมพันธ์กับลูกค้ารายนี้'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormSwitchField
|
<FormSwitchField
|
||||||
name='isActive'
|
name='isActive'
|
||||||
label='Active Record'
|
label='ใช้งานอยู่'
|
||||||
description='Inactive customers remain visible in history but should not be used for new work.'
|
description='ลูกค้าที่ปิดใช้งานยังคงอยู่ในประวัติ แต่ไม่ควรนำไปใช้กับงานใหม่'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form.Form>
|
</form.Form>
|
||||||
@@ -254,11 +252,11 @@ export function CustomerFormSheet({
|
|||||||
|
|
||||||
<SheetFooter>
|
<SheetFooter>
|
||||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
ยกเลิก
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' form='customer-form-sheet' isLoading={isPending}>
|
<Button type='submit' form='customer-form-sheet' isLoading={isPending}>
|
||||||
<Icons.check className='mr-2 h-4 w-4' />
|
<Icons.check className='mr-2 h-4 w-4' />
|
||||||
{isEdit ? 'Update Customer' : 'Create Customer'}
|
{isEdit ? 'อัปเดตลูกค้า' : 'สร้างลูกค้า'}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -276,7 +274,7 @@ export function CustomerFormSheetTrigger({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setOpen(true)}>
|
<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>
|
</Button>
|
||||||
<CustomerFormSheet open={open} onOpenChange={setOpen} referenceData={referenceData} />
|
<CustomerFormSheet open={open} onOpenChange={setOpen} referenceData={referenceData} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -24,42 +24,42 @@ export function DashboardApprovalMetrics({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Approval Analytics</CardTitle>
|
<CardTitle>ภาพรวมการอนุมัติเอกสาร</CardTitle>
|
||||||
<CardDescription>Live workflow counts plus the current approver queue.</CardDescription>
|
<CardDescription>สถิติ workflow ปัจจุบันและคิวงานที่รอผู้อนุมัติ</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-5'>
|
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-5'>
|
||||||
<Stat label='Pending Approvals' value={approvals.summary.pendingApprovals} />
|
<Stat label='รออนุมัติ' value={approvals.summary.pendingApprovals} />
|
||||||
<Stat label='Approved Today' value={approvals.summary.approvedToday} />
|
<Stat label='อนุมัติวันนี้' value={approvals.summary.approvedToday} />
|
||||||
<Stat label='Rejected Today' value={approvals.summary.rejectedToday} />
|
<Stat label='ไม่อนุมัติวันนี้' value={approvals.summary.rejectedToday} />
|
||||||
<Stat label='Returned Today' value={approvals.summary.returnedToday} />
|
<Stat label='ตีกลับวันนี้' value={approvals.summary.returnedToday} />
|
||||||
<Stat
|
<Stat
|
||||||
label='Average Approval Time'
|
label='เวลาอนุมัติเฉลี่ย'
|
||||||
value={approvals.summary.averageApprovalTimeHours}
|
value={approvals.summary.averageApprovalTimeHours}
|
||||||
suffix='hrs'
|
suffix='ชม.'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Document</TableHead>
|
<TableHead>เอกสาร</TableHead>
|
||||||
<TableHead>Workflow</TableHead>
|
<TableHead>Workflow</TableHead>
|
||||||
<TableHead>Current Step</TableHead>
|
<TableHead>ขั้นตอนปัจจุบัน</TableHead>
|
||||||
<TableHead>Requested At</TableHead>
|
<TableHead>วันที่ส่งอนุมัติ</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{approvals.myPendingApprovals.length === 0 ? (
|
{approvals.myPendingApprovals.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className='text-muted-foreground text-center'>
|
<TableCell colSpan={4} className='text-muted-foreground text-center'>
|
||||||
No pending approvals are assigned to your current approver scope.
|
ไม่มีงานอนุมัติที่อยู่ในขอบเขตการอนุมัติของคุณตอนนี้
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
approvals.myPendingApprovals.map((row) => (
|
approvals.myPendingApprovals.map((row) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell>
|
<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>
|
<div className='text-muted-foreground text-xs'>{row.entityTitle ?? '-'}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.workflowName}</TableCell>
|
<TableCell>{row.workflowName}</TableCell>
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export function DashboardFilters({
|
|||||||
value={params.branch ?? '__all__'}
|
value={params.branch ?? '__all__'}
|
||||||
onValueChange={(value) => void setParams({ branch: value === '__all__' ? null : value })}
|
onValueChange={(value) => void setParams({ branch: value === '__all__' ? null : value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Branch' />
|
<SelectValue placeholder='สาขา' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__all__'>All Branches</SelectItem>
|
<SelectItem value='__all__'>ทุกสาขา</SelectItem>
|
||||||
{referenceData.branches.map((item) => (
|
{referenceData.branches.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -77,11 +77,11 @@ export function DashboardFilters({
|
|||||||
value={params.salesman ?? '__all__'}
|
value={params.salesman ?? '__all__'}
|
||||||
onValueChange={(value) => void setParams({ salesman: value === '__all__' ? null : value })}
|
onValueChange={(value) => void setParams({ salesman: value === '__all__' ? null : value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Sales Person' />
|
<SelectValue placeholder='ฝ่ายขาย' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__all__'>All Sales</SelectItem>
|
<SelectItem value='__all__'>ฝ่ายขายทั้งหมด</SelectItem>
|
||||||
{referenceData.salesmen.map((item) => (
|
{referenceData.salesmen.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -95,11 +95,11 @@ export function DashboardFilters({
|
|||||||
void setParams({ projectPartyRole: value === '__all__' ? null : value })
|
void setParams({ projectPartyRole: value === '__all__' ? null : value })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Project Party Role' />
|
<SelectValue placeholder='บทบาทผู้เกี่ยวข้องในโครงการ' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__all__'>All Party Roles</SelectItem>
|
<SelectItem value='__all__'>ทุกบทบาท</SelectItem>
|
||||||
{referenceData.projectPartyRoles.map((item) => (
|
{referenceData.projectPartyRoles.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -111,11 +111,11 @@ export function DashboardFilters({
|
|||||||
value={params.productType ?? '__all__'}
|
value={params.productType ?? '__all__'}
|
||||||
onValueChange={(value) => void setParams({ productType: value === '__all__' ? null : value })}
|
onValueChange={(value) => void setParams({ productType: value === '__all__' ? null : value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder='Product Type' />
|
<SelectValue placeholder='ประเภทสินค้า' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='__all__'>All Product Types</SelectItem>
|
<SelectItem value='__all__'>ทุกประเภทสินค้า</SelectItem>
|
||||||
{referenceData.productTypes.map((item) => (
|
{referenceData.productTypes.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -138,7 +138,7 @@ export function DashboardFilters({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Reset Filters
|
ล้างตัวกรอง
|
||||||
</Button>
|
</Button>
|
||||||
{canExport ? (
|
{canExport ? (
|
||||||
<>
|
<>
|
||||||
@@ -148,7 +148,7 @@ export function DashboardFilters({
|
|||||||
exportQuery ? `&${exportQuery}` : ''
|
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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant='outline'>
|
<Button asChild variant='outline'>
|
||||||
@@ -157,7 +157,7 @@ export function DashboardFilters({
|
|||||||
exportQuery ? `&${exportQuery}` : ''
|
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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant='outline'>
|
<Button asChild variant='outline'>
|
||||||
@@ -166,7 +166,7 @@ export function DashboardFilters({
|
|||||||
exportQuery ? `&${exportQuery}` : ''
|
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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -21,31 +21,31 @@ export function DashboardFollowups({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Follow-up Dashboard</CardTitle>
|
<CardTitle>Dashboard งานติดตาม</CardTitle>
|
||||||
<CardDescription>Upcoming and completed CRM follow-ups across enquiries and quotations.</CardDescription>
|
<CardDescription>ภาพรวมงานติดตามที่กำลังจะถึงและที่ดำเนินการแล้วของโอกาสขายและใบเสนอราคา</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-4'>
|
<div className='grid gap-4 sm:grid-cols-2 xl:grid-cols-4'>
|
||||||
<Stat label='Due Today' value={followups.summary.dueToday} />
|
<Stat label='ครบกำหนดวันนี้' value={followups.summary.dueToday} />
|
||||||
<Stat label='Due This Week' value={followups.summary.dueThisWeek} />
|
<Stat label='ครบกำหนดสัปดาห์นี้' value={followups.summary.dueThisWeek} />
|
||||||
<Stat label='Overdue' value={followups.summary.overdue} />
|
<Stat label='เกินกำหนด' value={followups.summary.overdue} />
|
||||||
<Stat label='Completed' value={followups.summary.completed} />
|
<Stat label='เสร็จแล้ว' value={followups.summary.completed} />
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead>วันที่</TableHead>
|
||||||
<TableHead>Customer</TableHead>
|
<TableHead>ลูกค้า</TableHead>
|
||||||
<TableHead>Enquiry</TableHead>
|
<TableHead>โอกาสขาย</TableHead>
|
||||||
<TableHead>Enquiry Owner</TableHead>
|
<TableHead>ผู้ดูแลโอกาสขาย</TableHead>
|
||||||
<TableHead>Source</TableHead>
|
<TableHead>ที่มา</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{followups.upcoming.length === 0 ? (
|
{followups.upcoming.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className='text-muted-foreground text-center'>
|
<TableCell colSpan={5} className='text-muted-foreground text-center'>
|
||||||
No upcoming follow-ups match the current filters.
|
ไม่พบงานติดตามที่ตรงกับตัวกรองปัจจุบัน
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function DashboardFunnel({ steps }: { steps: CrmDashboardFunnelStep[] })
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sales Pipeline Funnel</CardTitle>
|
<CardTitle>Funnel การขาย</CardTitle>
|
||||||
<CardDescription>Lead, enquiry, quotation, approval, and won stages from the current pipeline model.</CardDescription>
|
<CardDescription>ภาพรวมตั้งแต่ลีด โอกาสขาย ใบเสนอราคา อนุมัติเอกสาร จนถึงงานที่ชนะ</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{steps.map((step) => (
|
{steps.map((step) => (
|
||||||
@@ -23,12 +23,12 @@ export function DashboardFunnel({ steps }: { steps: CrmDashboardFunnelStep[] })
|
|||||||
<div>
|
<div>
|
||||||
<div className='font-medium'>{step.label}</div>
|
<div className='font-medium'>{step.label}</div>
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{formatNumber(step.count)} items, value {formatNumber(step.value)}
|
{formatNumber(step.count)} รายการ, มูลค่า {formatNumber(step.value)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-right text-sm'>
|
<div className='text-right text-sm'>
|
||||||
<div>{step.conversionRate === null ? '-' : `${formatNumber(step.conversionRate)}%`}</div>
|
<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>
|
</div>
|
||||||
<div className='bg-muted h-2 overflow-hidden rounded-full'>
|
<div className='bg-muted h-2 overflow-hidden rounded-full'>
|
||||||
|
|||||||
@@ -12,25 +12,25 @@ export function DashboardHotProjects({ rows }: { rows: CrmDashboardHotProjectRow
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Top Active Enquiries</CardTitle>
|
<CardTitle>โอกาสขายเด่นที่กำลังดำเนินการ</CardTitle>
|
||||||
<CardDescription>Sales-owned hot enquiries ranked by estimated commercial value.</CardDescription>
|
<CardDescription>จัดอันดับโอกาสขายสำคัญตามมูลค่าที่ประเมินไว้</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Project</TableHead>
|
<TableHead>โครงการ</TableHead>
|
||||||
<TableHead>Customer</TableHead>
|
<TableHead>ลูกค้า</TableHead>
|
||||||
<TableHead className='text-right'>Value</TableHead>
|
<TableHead className='text-right'>มูลค่า</TableHead>
|
||||||
<TableHead>Enquiry Owner</TableHead>
|
<TableHead>ผู้ดูแลโอกาสขาย</TableHead>
|
||||||
<TableHead className='text-right'>Probability</TableHead>
|
<TableHead className='text-right'>ความน่าจะเป็น</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className='text-muted-foreground text-center'>
|
<TableCell colSpan={5} className='text-muted-foreground text-center'>
|
||||||
No hot opportunities match the current filters.
|
ไม่พบโอกาสขายเด่นที่ตรงกับตัวกรองปัจจุบัน
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ function RevenueTable({
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Customer</TableHead>
|
<TableHead>Customer</TableHead>
|
||||||
<TableHead className='text-right'>Revenue</TableHead>
|
<TableHead className='text-right'>มูลค่าใบเสนอราคา</TableHead>
|
||||||
<TableHead className='text-right'>Quotations</TableHead>
|
<TableHead className='text-right'>จำนวนใบเสนอราคา</TableHead>
|
||||||
<TableHead className='text-right'>Won Revenue</TableHead>
|
<TableHead className='text-right'>มูลค่างานที่ชนะ</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className='text-muted-foreground text-center'>
|
<TableCell colSpan={4} className='text-muted-foreground text-center'>
|
||||||
No revenue rows match the current filters.
|
ไม่พบข้อมูลที่ตรงกับตัวกรองปัจจุบัน
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
@@ -68,23 +68,23 @@ export function DashboardRevenueCards({
|
|||||||
return (
|
return (
|
||||||
<div className='grid gap-4 xl:grid-cols-2'>
|
<div className='grid gap-4 xl:grid-cols-2'>
|
||||||
<RevenueTable
|
<RevenueTable
|
||||||
title='Top End Customers'
|
title='เจ้าของโครงการมูลค่าสูงสุด'
|
||||||
description='Revenue owner attribution with billing-customer fallback when end-customer is missing.'
|
description='จัดอันดับตามเจ้าของโครงการ โดยใช้ลูกค้าผู้รับใบเสนอราคาเป็น fallback เมื่อไม่มี end customer'
|
||||||
rows={revenueAnalytics.topEndCustomers}
|
rows={revenueAnalytics.topEndCustomers}
|
||||||
/>
|
/>
|
||||||
<RevenueTable
|
<RevenueTable
|
||||||
title='Top Billing Customers'
|
title='ลูกค้าผู้รับใบเสนอราคามูลค่าสูงสุด'
|
||||||
description='Commercial relationship view grouped by billing customer.'
|
description='มุมมองเชิงพาณิชย์แบบจัดกลุ่มตามลูกค้าผู้รับใบเสนอราคา'
|
||||||
rows={revenueAnalytics.topBillingCustomers}
|
rows={revenueAnalytics.topBillingCustomers}
|
||||||
/>
|
/>
|
||||||
<RevenueTable
|
<RevenueTable
|
||||||
title='Top Contractors'
|
title='ผู้รับเหมามูลค่าสูงสุด'
|
||||||
description='Relationship analytics with full attribution for each contractor role.'
|
description='วิเคราะห์มูลค่าตามบทบาทผู้รับเหมาในโครงการ'
|
||||||
rows={revenueAnalytics.topContractors}
|
rows={revenueAnalytics.topContractors}
|
||||||
/>
|
/>
|
||||||
<RevenueTable
|
<RevenueTable
|
||||||
title='Top Consultants'
|
title='ที่ปรึกษามูลค่าสูงสุด'
|
||||||
description='Consultant-side revenue relationships from project parties.'
|
description='วิเคราะห์มูลค่าตามบทบาทที่ปรึกษาในโครงการ'
|
||||||
rows={revenueAnalytics.topConsultants}
|
rows={revenueAnalytics.topConsultants}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,27 +12,27 @@ export function DashboardSalesRanking({ rows }: { rows: CrmDashboardSalesRanking
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sales Ranking</CardTitle>
|
<CardTitle>อันดับผลงานฝ่ายขาย</CardTitle>
|
||||||
<CardDescription>Lead and enquiry ownership follow assignment rules. Quotation ownership uses salesman.</CardDescription>
|
<CardDescription>การนับลีดและโอกาสขายยึดตามผู้รับผิดชอบ ส่วนใบเสนอราคายึดตามผู้ขาย</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Sales Person</TableHead>
|
<TableHead>ฝ่ายขาย</TableHead>
|
||||||
<TableHead className='text-right'>Lead</TableHead>
|
<TableHead className='text-right'>ลีด</TableHead>
|
||||||
<TableHead className='text-right'>Enquiry</TableHead>
|
<TableHead className='text-right'>โอกาสขาย</TableHead>
|
||||||
<TableHead className='text-right'>Quotation</TableHead>
|
<TableHead className='text-right'>ใบเสนอราคา</TableHead>
|
||||||
<TableHead className='text-right'>Approved</TableHead>
|
<TableHead className='text-right'>อนุมัติแล้ว</TableHead>
|
||||||
<TableHead className='text-right'>Won Revenue</TableHead>
|
<TableHead className='text-right'>มูลค่างานที่ชนะ</TableHead>
|
||||||
<TableHead className='text-right'>Conversion</TableHead>
|
<TableHead className='text-right'>อัตราการเปลี่ยนสถานะ</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className='text-muted-foreground text-center'>
|
<TableCell colSpan={7} className='text-muted-foreground text-center'>
|
||||||
No sales ranking rows match the current filters.
|
ไม่พบข้อมูลอันดับผลงานที่ตรงกับตัวกรองปัจจุบัน
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -52,47 +52,47 @@ export function DashboardSummaryCards({
|
|||||||
return (
|
return (
|
||||||
<div className={`grid gap-4 ${canViewCommercialData ? 'xl:grid-cols-2 2xl:grid-cols-4' : 'xl:grid-cols-2'}`}>
|
<div className={`grid gap-4 ${canViewCommercialData ? 'xl:grid-cols-2 2xl:grid-cols-4' : 'xl:grid-cols-2'}`}>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title='Lead'
|
title='ลีด'
|
||||||
description='Marketing-owned lead-stage records based on pipeline stage.'
|
description='รายการที่ยังอยู่ในขั้นลีดและอยู่ในความรับผิดชอบของการตลาด'
|
||||||
href='/dashboard/crm/leads'
|
href='/dashboard/crm/leads'
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Lead Count', value: summary.lead.leadCount },
|
{ label: 'จำนวนลีด', value: summary.lead.leadCount },
|
||||||
{ label: 'New Leads', value: summary.lead.newLeads },
|
{ label: 'ลีดใหม่', value: summary.lead.newLeads },
|
||||||
{ label: 'Unassigned Leads', value: summary.lead.unassignedLeads },
|
{ label: 'ลีดยังไม่มอบหมาย', value: summary.lead.unassignedLeads },
|
||||||
{ label: 'Lead Aging Avg', value: summary.lead.averageLeadAgingDays, suffix: 'days' }
|
{ label: 'อายุลีดเฉลี่ย', value: summary.lead.averageLeadAgingDays, suffix: 'วัน' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title='Enquiry'
|
title='โอกาสขาย'
|
||||||
description='Sales-owned enquiry-stage records plus closed outcomes.'
|
description='รายการที่อยู่ในขั้นโอกาสขาย รวมถึงผลลัพธ์ชนะและแพ้'
|
||||||
href='/dashboard/crm/enquiries'
|
href='/dashboard/crm/enquiries'
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Enquiry Count', value: summary.enquiry.enquiryCount },
|
{ label: 'จำนวนโอกาสขาย', value: summary.enquiry.enquiryCount },
|
||||||
{ label: 'Active Enquiries', value: summary.enquiry.activeEnquiries },
|
{ label: 'โอกาสขายที่กำลังดำเนินการ', value: summary.enquiry.activeEnquiries },
|
||||||
{ label: 'Closed Won', value: summary.enquiry.wonCount },
|
{ label: 'จำนวนงานที่ชนะ', value: summary.enquiry.wonCount },
|
||||||
{ label: 'Closed Lost', value: summary.enquiry.lostCount }
|
{ label: 'จำนวนงานที่แพ้', value: summary.enquiry.lostCount }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{canViewCommercialData ? (
|
{canViewCommercialData ? (
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title='Quotation'
|
title='ใบเสนอราคา'
|
||||||
description='Live quotation status counts from production data.'
|
description='สถานะของใบเสนอราคาจากข้อมูลจริงในระบบ'
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Draft Quotations', value: summary.quotation.draftQuotations },
|
{ label: 'ฉบับร่าง', value: summary.quotation.draftQuotations },
|
||||||
{ label: 'Pending Approval', value: summary.quotation.pendingApproval },
|
{ label: 'รออนุมัติ', value: summary.quotation.pendingApproval },
|
||||||
{ label: 'Approved Quotations', value: summary.quotation.approvedQuotations },
|
{ label: 'อนุมัติแล้ว', value: summary.quotation.approvedQuotations },
|
||||||
{ label: 'Sent Quotations', value: summary.quotation.sentQuotations }
|
{ label: 'ส่งแล้ว', value: summary.quotation.sentQuotations }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{canViewCommercialData ? (
|
{canViewCommercialData ? (
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title='Revenue'
|
title='มูลค่าใบเสนอราคา'
|
||||||
description='Quotation, won, and lost values using governed revenue rules.'
|
description='มูลค่าใบเสนอราคา งานที่ชนะ และงานที่แพ้ตามกติกา revenue attribution'
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Quotation Value', value: summary.revenue.quotationValue },
|
{ label: 'มูลค่าใบเสนอราคา', value: summary.revenue.quotationValue },
|
||||||
{ label: 'Won Value', value: summary.revenue.wonValue },
|
{ label: 'มูลค่างานที่ชนะ', value: summary.revenue.wonValue },
|
||||||
{ label: 'Lost Value', value: summary.revenue.lostValue }
|
{ label: 'มูลค่างานที่แพ้', value: summary.revenue.lostValue }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
getRevenueByContractor,
|
getRevenueByContractor,
|
||||||
getRevenueByEndCustomer
|
getRevenueByEndCustomer
|
||||||
} from '@/features/crm/reporting/server/service';
|
} from '@/features/crm/reporting/server/service';
|
||||||
|
import { getPipelineStageThaiLabel, getProjectPartyRoleThaiLabel } from '@/features/crm/shared/terminology';
|
||||||
import type {
|
import type {
|
||||||
CrmDashboardApprovalRow,
|
CrmDashboardApprovalRow,
|
||||||
CrmDashboardApprovalSummary,
|
CrmDashboardApprovalSummary,
|
||||||
@@ -332,19 +333,19 @@ function buildFunnel(
|
|||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
key: 'lead',
|
key: 'lead',
|
||||||
label: 'Lead',
|
label: getPipelineStageThaiLabel('lead'),
|
||||||
count: summary.lead.leadCount,
|
count: summary.lead.leadCount,
|
||||||
value: 0
|
value: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enquiry',
|
key: 'enquiry',
|
||||||
label: 'Enquiry',
|
label: getPipelineStageThaiLabel('enquiry'),
|
||||||
count: summary.enquiry.enquiryCount,
|
count: summary.enquiry.enquiryCount,
|
||||||
value: summary.enquiry.enquiryValue
|
value: summary.enquiry.enquiryValue
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'quotation',
|
key: 'quotation',
|
||||||
label: 'Quotation',
|
label: 'ใบเสนอราคา',
|
||||||
count: scopedQuotations.filter(
|
count: scopedQuotations.filter(
|
||||||
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled'
|
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled'
|
||||||
).length,
|
).length,
|
||||||
@@ -354,7 +355,7 @@ function buildFunnel(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pending_approval',
|
key: 'pending_approval',
|
||||||
label: 'Pending Approval',
|
label: 'รออนุมัติ',
|
||||||
count: scopedQuotations.filter(
|
count: scopedQuotations.filter(
|
||||||
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'pending_approval'
|
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'pending_approval'
|
||||||
).length,
|
).length,
|
||||||
@@ -364,7 +365,7 @@ function buildFunnel(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'approved',
|
key: 'approved',
|
||||||
label: 'Approved',
|
label: 'อนุมัติแล้ว',
|
||||||
count: scopedQuotations.filter(
|
count: scopedQuotations.filter(
|
||||||
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'approved'
|
(row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'approved'
|
||||||
).length,
|
).length,
|
||||||
@@ -374,7 +375,7 @@ function buildFunnel(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'closed_won',
|
key: 'closed_won',
|
||||||
label: 'Closed Won',
|
label: getPipelineStageThaiLabel('closed_won'),
|
||||||
count: summary.enquiry.wonCount,
|
count: summary.enquiry.wonCount,
|
||||||
value: scopedQuotations
|
value: scopedQuotations
|
||||||
.filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'accepted')
|
.filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'accepted')
|
||||||
|
|||||||
@@ -57,21 +57,21 @@ export function EnquiryAssignmentDialog({
|
|||||||
const assignMutation = useMutation({
|
const assignMutation = useMutation({
|
||||||
...assignEnquiryMutation,
|
...assignEnquiryMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Lead converted to enquiry successfully');
|
toast.success('ส่งต่อลีดไปยังฝ่ายขายสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to assign enquiry')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถมอบหมายโอกาสขายได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const reassignMutation = useMutation({
|
const reassignMutation = useMutation({
|
||||||
...reassignEnquiryMutation,
|
...reassignEnquiryMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Enquiry reassigned successfully');
|
toast.success('เปลี่ยนผู้ดูแลโอกาสขายสำเร็จ');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to reassign enquiry')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถเปลี่ยนผู้ดูแลโอกาสขายได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
@@ -109,11 +109,11 @@ export function EnquiryAssignmentDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{mode === 'assign' ? 'Convert To Enquiry' : 'Reassign Enquiry Owner'}</DialogTitle>
|
<DialogTitle>{mode === 'assign' ? 'ส่งต่อให้ฝ่ายขาย' : 'เปลี่ยนผู้ดูแลโอกาสขาย'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{mode === 'assign'
|
{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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -121,24 +121,24 @@ export function EnquiryAssignmentDialog({
|
|||||||
<form.Form id='enquiry-assignment-form' className='px-0 md:px-0'>
|
<form.Form id='enquiry-assignment-form' className='px-0 md:px-0'>
|
||||||
{noUsersAvailable ? (
|
{noUsersAvailable ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='assignedToUserId'
|
name='assignedToUserId'
|
||||||
label='Enquiry Owner'
|
label='ผู้ดูแลโอกาสขาย'
|
||||||
required
|
required
|
||||||
options={referenceData.assignableUsers.map((user) => ({
|
options={referenceData.assignableUsers.map((user) => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
label: `${user.name} (${user.businessRole.replaceAll('_', ' ')})`
|
label: `${user.name} (${user.businessRole.replaceAll('_', ' ')})`
|
||||||
}))}
|
}))}
|
||||||
placeholder='Select sales user'
|
placeholder='เลือกผู้ใช้ฝ่ายขาย'
|
||||||
/>
|
/>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='remark'
|
name='remark'
|
||||||
label='Remark'
|
label='หมายเหตุ'
|
||||||
placeholder='Optional handoff context or assignment note'
|
placeholder='บริบทเพิ่มเติมหรือหมายเหตุประกอบการส่งต่องาน'
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -147,7 +147,7 @@ export function EnquiryAssignmentDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
ยกเลิก
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
@@ -156,7 +156,7 @@ export function EnquiryAssignmentDialog({
|
|||||||
disabled={noUsersAvailable}
|
disabled={noUsersAvailable}
|
||||||
>
|
>
|
||||||
<Icons.userPen className='mr-2 h-4 w-4' />
|
<Icons.userPen className='mr-2 h-4 w-4' />
|
||||||
{mode === 'assign' ? 'Convert To Enquiry' : 'Reassign Owner'}
|
{mode === 'assign' ? 'ส่งต่อให้ฝ่ายขาย' : 'เปลี่ยนผู้ดูแล'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ export function EnquiryCellAction({
|
|||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteEnquiryMutation,
|
...deleteEnquiryMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${workspace === 'lead' ? 'Lead' : 'Enquiry'} deleted successfully`);
|
toast.success(`${workspace === 'lead' ? 'ลบลีดสำเร็จ' : 'ลบโอกาสขายสำเร็จ'}`);
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: `Failed to delete ${workspace === 'lead' ? 'lead' : 'enquiry'}`
|
: `${workspace === 'lead' ? 'ไม่สามารถลบลีดได้' : 'ไม่สามารถลบโอกาสขายได้'}`
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export function EnquiryCellAction({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>การดำเนินการ</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
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>
|
||||||
<DropdownMenuItem onClick={() => setEditOpen(true)} disabled={!canUpdate}>
|
<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>
|
||||||
<DropdownMenuItem onClick={() => setDeleteOpen(true)} disabled={!canDelete}>
|
<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>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -203,11 +203,11 @@ function getEnquiryWorkspaceColumns({
|
|||||||
id: 'customer',
|
id: 'customer',
|
||||||
accessorFn: (row) => row.customerId,
|
accessorFn: (row) => row.customerId,
|
||||||
header: ({ column }: { column: Column<EnquiryListItem, unknown> }) => (
|
header: ({ column }: { column: Column<EnquiryListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Billing Customer' />
|
<DataTableColumnHeader column={column} title='ลูกค้าผู้รับใบเสนอราคา' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => <span>{row.original.customerName}</span>,
|
cell: ({ row }) => <span>{row.original.customerName}</span>,
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Billing Customer',
|
label: 'ลูกค้าผู้รับใบเสนอราคา',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.customers.map((item) => ({
|
options: referenceData.customers.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import type { QuotationRelationItem } from '@/features/crm/quotations/api/types';
|
import type { QuotationRelationItem } from '@/features/crm/quotations/api/types';
|
||||||
|
import { getPipelineStageThaiLabel } from '@/features/crm/shared/terminology';
|
||||||
import { enquiryByIdOptions, enquiryProjectPartiesOptions } from '../api/queries';
|
import { enquiryByIdOptions, enquiryProjectPartiesOptions } from '../api/queries';
|
||||||
import type { EnquiryRecord, EnquiryReferenceData } from '../api/types';
|
import type { EnquiryRecord, EnquiryReferenceData } from '../api/types';
|
||||||
import { EnquiryAssignmentDialog } from './enquiry-assignment-dialog';
|
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']) {
|
function getPipelineStageLabel(stage: EnquiryRecord['pipelineStage']) {
|
||||||
switch (stage) {
|
return getPipelineStageThaiLabel(stage);
|
||||||
case 'lead':
|
|
||||||
return 'Lead';
|
|
||||||
case 'enquiry':
|
|
||||||
return 'Enquiry';
|
|
||||||
case 'closed_won':
|
|
||||||
return 'Closed Won';
|
|
||||||
case 'closed_lost':
|
|
||||||
return 'Closed Lost';
|
|
||||||
default:
|
|
||||||
return stage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnquiryDetailProps {
|
interface EnquiryDetailProps {
|
||||||
@@ -109,26 +99,26 @@ export function EnquiryDetail({
|
|||||||
const pipelineStageLabel = getPipelineStageLabel(enquiry.pipelineStage);
|
const pipelineStageLabel = getPipelineStageLabel(enquiry.pipelineStage);
|
||||||
const isLeadWorkspace = workspace === 'lead';
|
const isLeadWorkspace = workspace === 'lead';
|
||||||
const backHref = isLeadWorkspace ? '/dashboard/crm/leads' : '/dashboard/crm/enquiries';
|
const backHref = isLeadWorkspace ? '/dashboard/crm/leads' : '/dashboard/crm/enquiries';
|
||||||
const recordLabel = isLeadWorkspace ? 'Lead' : 'Enquiry';
|
const recordLabel = isLeadWorkspace ? 'ลีด' : 'โอกาสขาย';
|
||||||
const ownerLabel = isLeadWorkspace ? 'Assigned Sales' : 'Enquiry Owner';
|
const ownerLabel = isLeadWorkspace ? 'ฝ่ายขายที่รับผิดชอบ' : 'ผู้ดูแลโอกาสขาย';
|
||||||
const infoCardTitle = isLeadWorkspace ? 'Lead Information' : 'Enquiry Information';
|
const infoCardTitle = isLeadWorkspace ? 'ข้อมูลลีด' : 'ข้อมูลโอกาสขาย';
|
||||||
const infoCardDescription = isLeadWorkspace
|
const infoCardDescription = isLeadWorkspace
|
||||||
? 'Lead source, qualification context, and sales handoff readiness.'
|
? 'ที่มาของลีด ข้อมูลการคัดกรอง และความพร้อมในการส่งต่อให้ฝ่ายขาย'
|
||||||
: 'Project scope, ownership context, and active sales execution details.';
|
: 'ขอบเขตงาน ข้อมูลผู้รับผิดชอบ และรายละเอียดการดูแลของฝ่ายขาย';
|
||||||
const assignmentButtonLabel =
|
const assignmentButtonLabel =
|
||||||
assignmentMode === 'assign'
|
assignmentMode === 'assign'
|
||||||
? isLeadWorkspace
|
? isLeadWorkspace
|
||||||
? 'Assign To Sales'
|
? 'ส่งต่อให้ฝ่ายขาย'
|
||||||
: 'Assign Enquiry Owner'
|
: 'มอบหมายผู้ดูแลโอกาสขาย'
|
||||||
: isLeadWorkspace
|
: isLeadWorkspace
|
||||||
? 'Reassign Sales Owner'
|
? 'เปลี่ยนฝ่ายขายที่รับผิดชอบ'
|
||||||
: 'Reassign Enquiry Owner';
|
: 'เปลี่ยนผู้ดูแลโอกาสขาย';
|
||||||
const projectSectionLabel = isLeadWorkspace ? 'Lead Scope' : 'Project Information';
|
const projectSectionLabel = isLeadWorkspace ? 'ขอบเขตลีด' : 'ข้อมูลโครงการ';
|
||||||
const followupTabLabel = isLeadWorkspace ? 'Lead Follow-ups' : 'Follow-ups';
|
const followupTabLabel = isLeadWorkspace ? 'งานติดตามลีด' : 'งานติดตาม';
|
||||||
const relatedTabLabel = isLeadWorkspace ? 'Related Quotations' : 'Quotations';
|
const relatedTabLabel = isLeadWorkspace ? 'ใบเสนอราคาที่เกี่ยวข้อง' : 'ใบเสนอราคา';
|
||||||
const snapshotDescription = isLeadWorkspace
|
const snapshotDescription = isLeadWorkspace
|
||||||
? 'Operational metadata and lead ownership handoff state for this record.'
|
? 'ข้อมูลระบบและสถานะการส่งต่อของลีดรายการนี้'
|
||||||
: 'Operational metadata and assignment state for this enquiry record.';
|
: 'ข้อมูลระบบและสถานะการรับผิดชอบของโอกาสขายรายการนี้';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
@@ -147,7 +137,7 @@ export function EnquiryDetail({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-semibold'>{enquiry.title}</h2>
|
<h2 className='text-2xl font-semibold'>{enquiry.title}</h2>
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
{pipelineStageLabel} | {customer?.name ?? 'Unknown customer'}
|
{pipelineStageLabel} | {customer?.name ?? 'ไม่ทราบชื่อลูกค้า'}
|
||||||
{contact ? ` | ${contact.name}` : ''}
|
{contact ? ` | ${contact.name}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,9 +164,9 @@ export function EnquiryDetail({
|
|||||||
<CardContent className='pt-6'>
|
<CardContent className='pt-6'>
|
||||||
<Tabs defaultValue='overview' className='gap-4'>
|
<Tabs defaultValue='overview' className='gap-4'>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='overview'>Overview</TabsTrigger>
|
<TabsTrigger value='overview'>ภาพรวม</TabsTrigger>
|
||||||
<TabsTrigger value='followups'>{followupTabLabel}</TabsTrigger>
|
<TabsTrigger value='followups'>{followupTabLabel}</TabsTrigger>
|
||||||
<TabsTrigger value='activity'>Activity</TabsTrigger>
|
<TabsTrigger value='activity'>กิจกรรม</TabsTrigger>
|
||||||
{canViewRelatedQuotations ? (
|
{canViewRelatedQuotations ? (
|
||||||
<TabsTrigger value='related'>{relatedTabLabel}</TabsTrigger>
|
<TabsTrigger value='related'>{relatedTabLabel}</TabsTrigger>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -189,29 +179,29 @@ export function EnquiryDetail({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label={isLeadWorkspace ? 'Company' : 'Billing Customer'}
|
label={isLeadWorkspace ? 'ลูกค้า' : 'ลูกค้าผู้รับใบเสนอราคา'}
|
||||||
value={customer?.name}
|
value={customer?.name}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Contact' value={contact?.name} />
|
<FieldItem label='ผู้ติดต่อ' value={contact?.name} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Branch'
|
label='สาขา'
|
||||||
value={enquiry.branchId ? branchMap.get(enquiry.branchId) : null}
|
value={enquiry.branchId ? branchMap.get(enquiry.branchId) : null}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Active' value={enquiry.isActive ? 'Yes' : 'No'} />
|
<FieldItem label='ใช้งานอยู่' value={enquiry.isActive ? 'ใช่' : 'ไม่ใช่'} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Product Type'
|
label='ประเภทสินค้า'
|
||||||
value={productTypeMap.get(enquiry.productType)?.label}
|
value={productTypeMap.get(enquiry.productType)?.label}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Priority'
|
label='ความสำคัญ'
|
||||||
value={priorityMap.get(enquiry.priority)?.label}
|
value={priorityMap.get(enquiry.priority)?.label}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Lead Source'
|
label='ที่มาของลีด'
|
||||||
value={leadChannelMap.get(enquiry.leadChannel ?? '')?.label}
|
value={leadChannelMap.get(enquiry.leadChannel ?? '')?.label}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Expected Close Date'
|
label='วันที่คาดว่าจะปิดการขาย'
|
||||||
value={
|
value={
|
||||||
enquiry.expectedCloseDate
|
enquiry.expectedCloseDate
|
||||||
? new Date(enquiry.expectedCloseDate).toLocaleDateString()
|
? new Date(enquiry.expectedCloseDate).toLocaleDateString()
|
||||||
@@ -219,7 +209,7 @@ export function EnquiryDetail({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Estimated Value'
|
label='มูลค่าประมาณการ'
|
||||||
value={
|
value={
|
||||||
enquiry.estimatedValue !== null
|
enquiry.estimatedValue !== null
|
||||||
? enquiry.estimatedValue.toLocaleString()
|
? enquiry.estimatedValue.toLocaleString()
|
||||||
@@ -227,31 +217,31 @@ export function EnquiryDetail({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Chance %'
|
label='โอกาสปิดการขาย %'
|
||||||
value={enquiry.chancePercent !== null ? `${enquiry.chancePercent}%` : null}
|
value={enquiry.chancePercent !== null ? `${enquiry.chancePercent}%` : null}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Competitor' value={enquiry.competitor} />
|
<FieldItem label='คู่แข่ง' value={enquiry.competitor} />
|
||||||
<FieldItem label='Source' value={enquiry.source} />
|
<FieldItem label='ที่มา' value={enquiry.source} />
|
||||||
<FieldItem label='Pipeline Stage' value={pipelineStageLabel} />
|
<FieldItem label='ขั้นตอนใน Pipeline' value={pipelineStageLabel} />
|
||||||
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
|
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Assigned At'
|
label='มอบหมายเมื่อ'
|
||||||
value={
|
value={
|
||||||
enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null
|
enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Assigned By' value={enquiry.assignedByName} />
|
<FieldItem label='มอบหมายโดย' value={enquiry.assignedByName} />
|
||||||
<FieldItem label='Assignment Remark' value={enquiry.assignmentRemark} />
|
<FieldItem label='หมายเหตุการมอบหมาย' value={enquiry.assignmentRemark} />
|
||||||
<div className='md:col-span-2 xl:col-span-4'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{isLeadWorkspace ? 'Lead Parties' : 'Project Parties'}
|
{isLeadWorkspace ? 'ผู้เกี่ยวข้องของลีด' : 'ผู้เกี่ยวข้องในโครงการ'}
|
||||||
</div>
|
</div>
|
||||||
{!projectPartiesData.items.length ? (
|
{!projectPartiesData.items.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||||
{isLeadWorkspace
|
{isLeadWorkspace
|
||||||
? 'No lead parties have been added yet.'
|
? 'ยังไม่มีผู้เกี่ยวข้องของลีด'
|
||||||
: 'No project parties have been added yet.'}
|
: 'ยังไม่มีผู้เกี่ยวข้องในโครงการ'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-3'>
|
<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 key={item.id} className='rounded-lg border p-4 text-sm'>
|
||||||
<div className='font-medium'>{item.customerName}</div>
|
<div className='font-medium'>{item.customerName}</div>
|
||||||
<div className='text-muted-foreground'>
|
<div className='text-muted-foreground'>
|
||||||
Role: {item.roleLabel ?? item.roleCode}
|
บทบาท: {item.roleLabel ?? item.roleCode}
|
||||||
</div>
|
</div>
|
||||||
{item.remark ? <div className='mt-1'>{item.remark}</div> : null}
|
{item.remark ? <div className='mt-1'>{item.remark}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -278,13 +268,13 @@ export function EnquiryDetail({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 xl:col-span-4'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<FieldItem label='Description' value={enquiry.description} />
|
<FieldItem label='รายละเอียด' value={enquiry.description} />
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 xl:col-span-4'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<FieldItem label='Requirement' value={enquiry.requirement} />
|
<FieldItem label='ความต้องการ' value={enquiry.requirement} />
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 xl:col-span-4'>
|
<div className='md:col-span-2 xl:col-span-4'>
|
||||||
<FieldItem label='Notes' value={enquiry.notes} />
|
<FieldItem label='หมายเหตุ' value={enquiry.notes} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -302,15 +292,13 @@ export function EnquiryDetail({
|
|||||||
<TabsContent value='activity'>
|
<TabsContent value='activity'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{isLeadWorkspace ? 'Lead Activity' : 'Activity'}</CardTitle>
|
<CardTitle>{isLeadWorkspace ? 'กิจกรรมของลีด' : 'กิจกรรม'}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>ประวัติการเปลี่ยนแปลงของรายการนี้และงานติดตามที่เกี่ยวข้อง</CardDescription>
|
||||||
Audit events for this record and its follow-up history.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{!data.activity.length ? (
|
{!data.activity.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||||
No activity recorded yet.
|
ยังไม่มีกิจกรรม
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.activity.map((item, index) => (
|
data.activity.map((item, index) => (
|
||||||
@@ -347,13 +335,13 @@ export function EnquiryDetail({
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{relatedTabLabel}</CardTitle>
|
<CardTitle>{relatedTabLabel}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Production quotations already linked to this record.
|
ใบเสนอราคาที่เชื่อมกับรายการนี้แล้ว
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!relatedQuotations.length ? (
|
{!relatedQuotations.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
@@ -388,21 +376,21 @@ export function EnquiryDetail({
|
|||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Record Snapshot</CardTitle>
|
<CardTitle>ข้อมูลระบบ</CardTitle>
|
||||||
<CardDescription>{snapshotDescription}</CardDescription>
|
<CardDescription>{snapshotDescription}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
<FieldItem label='Pipeline Stage' value={pipelineStageLabel} />
|
<FieldItem label='ขั้นตอนใน Pipeline' value={pipelineStageLabel} />
|
||||||
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
|
<FieldItem label={ownerLabel} value={enquiry.assignedToName} />
|
||||||
<FieldItem label='Assigned By' value={enquiry.assignedByName} />
|
<FieldItem label='มอบหมายโดย' value={enquiry.assignedByName} />
|
||||||
<FieldItem label='Created By' value={enquiry.createdBy} />
|
<FieldItem label='สร้างโดย' value={enquiry.createdBy} />
|
||||||
<FieldItem label='Updated By' value={enquiry.updatedBy} />
|
<FieldItem label='อัปเดตโดย' value={enquiry.updatedBy} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Assigned At'
|
label='มอบหมายเมื่อ'
|
||||||
value={enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null}
|
value={enquiry.assignedAt ? new Date(enquiry.assignedAt).toLocaleString() : null}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Created At' value={new Date(enquiry.createdAt).toLocaleString()} />
|
<FieldItem label='สร้างเมื่อ' value={new Date(enquiry.createdAt).toLocaleString()} />
|
||||||
<FieldItem label='Updated At' value={new Date(enquiry.updatedAt).toLocaleString()} />
|
<FieldItem label='อัปเดตเมื่อ' value={new Date(enquiry.updatedAt).toLocaleString()} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,22 +38,20 @@ export function EnquiryFollowupsTab({
|
|||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteEnquiryFollowupMutation,
|
...deleteEnquiryFollowupMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Follow-up deleted successfully');
|
toast.success('ลบงานติดตามสำเร็จ');
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to delete follow-up')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบงานติดตามได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Follow-ups</CardTitle>
|
<CardTitle>งานติดตาม</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>ติดตามการติดต่อ ผลลัพธ์ และขั้นตอนถัดไปของโอกาสขายนี้</CardDescription>
|
||||||
Track the latest contact moments, outcomes, and next steps for this enquiry.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
{canCreate ? (
|
{canCreate ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -63,14 +61,14 @@ export function EnquiryFollowupsTab({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.add className='mr-2 h-4 w-4' /> Add Follow-up
|
<Icons.add className='mr-2 h-4 w-4' /> เพิ่มงานติดตาม
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{!data.items.length ? (
|
{!data.items.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||||
No follow-ups recorded yet.
|
ยังไม่มีงานติดตาม
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.items.map((item) => (
|
data.items.map((item) => (
|
||||||
@@ -89,11 +87,11 @@ export function EnquiryFollowupsTab({
|
|||||||
</div>
|
</div>
|
||||||
{item.outcome ? <p className='text-sm'>{item.outcome}</p> : null}
|
{item.outcome ? <p className='text-sm'>{item.outcome}</p> : null}
|
||||||
{item.nextAction ? (
|
{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}
|
) : null}
|
||||||
{item.nextFollowupDate ? (
|
{item.nextFollowupDate ? (
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
Next follow-up: {new Date(item.nextFollowupDate).toLocaleDateString()}
|
นัดติดตามครั้งถัดไป: {new Date(item.nextFollowupDate).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{item.notes ? <p className='text-muted-foreground text-sm'>{item.notes}</p> : null}
|
{item.notes ? <p className='text-muted-foreground text-sm'>{item.notes}</p> : null}
|
||||||
@@ -108,7 +106,7 @@ export function EnquiryFollowupsTab({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.edit className='mr-2 h-4 w-4' /> Edit
|
<Icons.edit className='mr-2 h-4 w-4' /> แก้ไข
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -119,7 +117,7 @@ export function EnquiryFollowupsTab({
|
|||||||
setDeleteOpen(true);
|
setDeleteOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.trash className='mr-2 h-4 w-4' /> Delete
|
<Icons.trash className='mr-2 h-4 w-4' /> ลบ
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,21 +90,21 @@ export function EnquiryFormSheet({
|
|||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
...createEnquiryMutation,
|
...createEnquiryMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${recordLabel} created successfully`);
|
toast.success(`สร้าง${recordLabel}สำเร็จ`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
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({
|
const updateMutation = useMutation({
|
||||||
...updateEnquiryMutation,
|
...updateEnquiryMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${recordLabel} updated successfully`);
|
toast.success(`อัปเดต${recordLabel}สำเร็จ`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
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(
|
const contactsForCustomer = referenceData.contacts.filter(
|
||||||
@@ -179,17 +179,17 @@ export function EnquiryFormSheet({
|
|||||||
|
|
||||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
const recordLabel = workspace === 'lead' ? 'Lead' : 'Enquiry';
|
const recordLabel = workspace === 'lead' ? 'ลีด' : 'โอกาสขาย';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className='flex flex-col sm:max-w-4xl'>
|
<SheetContent className='flex flex-col sm:max-w-4xl'>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{isEdit ? `Edit ${recordLabel}` : `New ${recordLabel}`}</SheetTitle>
|
<SheetTitle>{isEdit ? `แก้ไข${recordLabel}` : `เพิ่ม${recordLabel}`}</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
{workspace === 'lead'
|
{workspace === 'lead'
|
||||||
? 'Capture marketing lead details before assignment to sales.'
|
? 'บันทึกข้อมูลลีดของฝ่ายการตลาดก่อนส่งต่อให้ฝ่ายขาย'
|
||||||
: 'Capture sales enquiry details before quotation work begins.'}
|
: 'บันทึกข้อมูลโอกาสขายของฝ่ายขายก่อนเริ่มทำใบเสนอราคา'}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ export function EnquiryFormSheet({
|
|||||||
children={(field) => (
|
children={(field) => (
|
||||||
<field.FieldSet>
|
<field.FieldSet>
|
||||||
<field.Field>
|
<field.Field>
|
||||||
<field.FieldLabel>Billing Customer *</field.FieldLabel>
|
<field.FieldLabel>ลูกค้าผู้รับใบเสนอราคา *</field.FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
value={field.state.value ?? ''}
|
value={field.state.value ?? ''}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -211,8 +211,8 @@ export function EnquiryFormSheet({
|
|||||||
form.setFieldValue('contactId', '');
|
form.setFieldValue('contactId', '');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger aria-label='Customer'>
|
<SelectTrigger aria-label='ลูกค้า'>
|
||||||
<SelectValue placeholder='Select customer' />
|
<SelectValue placeholder='เลือกลูกค้า' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{referenceData.customers.map((customer) => (
|
{referenceData.customers.map((customer) => (
|
||||||
@@ -233,7 +233,7 @@ export function EnquiryFormSheet({
|
|||||||
children={(field) => (
|
children={(field) => (
|
||||||
<field.FieldSet>
|
<field.FieldSet>
|
||||||
<field.Field>
|
<field.Field>
|
||||||
<field.FieldLabel>Contact</field.FieldLabel>
|
<field.FieldLabel>ผู้ติดต่อ</field.FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
value={field.state.value ?? ''}
|
value={field.state.value ?? ''}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -241,19 +241,19 @@ export function EnquiryFormSheet({
|
|||||||
field.handleBlur();
|
field.handleBlur();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger aria-label='Contact'>
|
<SelectTrigger aria-label='ผู้ติดต่อ'>
|
||||||
<SelectValue placeholder='Select contact' />
|
<SelectValue placeholder='เลือกผู้ติดต่อ' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{contactsForCustomer.length ? (
|
{contactsForCustomer.length ? (
|
||||||
contactsForCustomer.map((contact) => (
|
contactsForCustomer.map((contact) => (
|
||||||
<SelectItem key={contact.id} value={contact.id}>
|
<SelectItem key={contact.id} value={contact.id}>
|
||||||
{contact.name}
|
{contact.name}
|
||||||
{contact.isPrimary ? ' (Primary)' : ''}
|
{contact.isPrimary ? ' (ผู้ติดต่อหลัก)' : ''}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<SelectItem value='__none__'>No contact selected</SelectItem>
|
<SelectItem value='__none__'>ไม่เลือกผู้ติดต่อ</SelectItem>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -265,28 +265,28 @@ export function EnquiryFormSheet({
|
|||||||
|
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='title'
|
name='title'
|
||||||
label='Title'
|
label='ชื่อรายการ'
|
||||||
required
|
required
|
||||||
placeholder='Warehouse crane upgrade enquiry'
|
placeholder='Warehouse crane upgrade enquiry'
|
||||||
/>
|
/>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='projectName'
|
name='projectName'
|
||||||
label='Project Name'
|
label='ชื่อโครงการ'
|
||||||
placeholder='Bangna Warehouse Expansion'
|
placeholder='Bangna Warehouse Expansion'
|
||||||
/>
|
/>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='projectLocation'
|
name='projectLocation'
|
||||||
label='Project Location'
|
label='สถานที่โครงการ'
|
||||||
placeholder='Bangkok'
|
placeholder='Bangkok'
|
||||||
/>
|
/>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='source'
|
name='source'
|
||||||
label='Source'
|
label='ที่มา'
|
||||||
placeholder='Existing customer referral'
|
placeholder='Existing customer referral'
|
||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='branchId'
|
name='branchId'
|
||||||
label='Branch'
|
label='สาขา'
|
||||||
options={referenceData.branches.map((branch) => ({
|
options={referenceData.branches.map((branch) => ({
|
||||||
value: branch.id,
|
value: branch.id,
|
||||||
label: branch.name
|
label: branch.name
|
||||||
@@ -294,7 +294,7 @@ export function EnquiryFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='productType'
|
name='productType'
|
||||||
label='Product Type'
|
label='ประเภทสินค้า'
|
||||||
required
|
required
|
||||||
options={referenceData.productTypes.map((item) => ({
|
options={referenceData.productTypes.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -303,7 +303,7 @@ export function EnquiryFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='status'
|
name='status'
|
||||||
label='Status'
|
label='สถานะ'
|
||||||
required
|
required
|
||||||
options={referenceData.statuses.map((item) => ({
|
options={referenceData.statuses.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -312,7 +312,7 @@ export function EnquiryFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='priority'
|
name='priority'
|
||||||
label='Priority'
|
label='ความสำคัญ'
|
||||||
required
|
required
|
||||||
options={referenceData.priorities.map((item) => ({
|
options={referenceData.priorities.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -321,7 +321,7 @@ export function EnquiryFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormSelectField
|
<FormSelectField
|
||||||
name='leadChannel'
|
name='leadChannel'
|
||||||
label='Lead Channel'
|
label='ที่มาของลีด'
|
||||||
options={referenceData.leadChannels.map((item) => ({
|
options={referenceData.leadChannels.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.label
|
label: item.label
|
||||||
@@ -329,48 +329,48 @@ export function EnquiryFormSheet({
|
|||||||
/>
|
/>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='estimatedValue'
|
name='estimatedValue'
|
||||||
label='Estimated Value'
|
label='มูลค่าประมาณการ'
|
||||||
type='number'
|
type='number'
|
||||||
placeholder='2500000'
|
placeholder='2500000'
|
||||||
/>
|
/>
|
||||||
<FormTextField name='chancePercent' label='Chance %' type='number' placeholder='60' />
|
<FormTextField name='chancePercent' label='โอกาสปิดการขาย %' type='number' placeholder='60' />
|
||||||
<FormTextField
|
<FormTextField
|
||||||
name='expectedCloseDate'
|
name='expectedCloseDate'
|
||||||
label='Expected Close Date'
|
label='วันที่คาดว่าจะปิดการขาย'
|
||||||
placeholder='YYYY-MM-DD'
|
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'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='description'
|
name='description'
|
||||||
label='Description'
|
label='รายละเอียด'
|
||||||
placeholder='High-level summary of the lead or enquiry'
|
placeholder='สรุปภาพรวมของลีดหรือโอกาสขาย'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='requirement'
|
name='requirement'
|
||||||
label='Requirement'
|
label='ความต้องการ'
|
||||||
placeholder='Specific technical or commercial requirements'
|
placeholder='ความต้องการเชิงเทคนิคหรือเชิงพาณิชย์'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<FormTextareaField
|
<FormTextareaField
|
||||||
name='notes'
|
name='notes'
|
||||||
label='Notes'
|
label='หมายเหตุ'
|
||||||
placeholder='Internal notes, blockers, or handoff guidance'
|
placeholder='หมายเหตุภายใน อุปสรรค หรือข้อมูลประกอบการส่งต่องาน'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
||||||
<FormSwitchField
|
<FormSwitchField
|
||||||
name='isHotProject'
|
name='isHotProject'
|
||||||
label='Hot Project'
|
label='โครงการด่วน'
|
||||||
description='Use for urgent or highly strategic enquiries.'
|
description='ใช้สำหรับงานเร่งด่วนหรือโครงการสำคัญเชิงกลยุทธ์'
|
||||||
/>
|
/>
|
||||||
<FormSwitchField
|
<FormSwitchField
|
||||||
name='isActive'
|
name='isActive'
|
||||||
label='Active Record'
|
label='ใช้งานอยู่'
|
||||||
description='Inactive leads and enquiries stay in history but should not progress further.'
|
description='รายการที่ปิดใช้งานจะยังอยู่ในประวัติ แต่ไม่ควรถูกดำเนินการต่อ'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
@@ -387,11 +387,11 @@ export function EnquiryFormSheet({
|
|||||||
|
|
||||||
<SheetFooter>
|
<SheetFooter>
|
||||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
ยกเลิก
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' form='enquiry-form-sheet' isLoading={isPending}>
|
<Button type='submit' form='enquiry-form-sheet' isLoading={isPending}>
|
||||||
<Icons.check className='mr-2 h-4 w-4' />
|
<Icons.check className='mr-2 h-4 w-4' />
|
||||||
{isEdit ? `Update ${recordLabel}` : `Create ${recordLabel}`}
|
{isEdit ? `อัปเดต${recordLabel}` : `สร้าง${recordLabel}`}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -411,7 +411,7 @@ export function EnquiryFormSheetTrigger({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setOpen(true)}>
|
<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>
|
</Button>
|
||||||
<EnquiryFormSheet
|
<EnquiryFormSheet
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ export function QuotationCellAction({
|
|||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...deleteQuotationMutation,
|
...deleteQuotationMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Quotation deleted successfully');
|
toast.success('ลบใบเสนอราคาสำเร็จ');
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to delete quotation')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถลบใบเสนอราคาได้')
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,15 +65,15 @@ export function QuotationCellAction({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>การดำเนินการ</DropdownMenuLabel>
|
||||||
<DropdownMenuItem onClick={() => router.push(`/dashboard/crm/quotations/${data.id}`)}>
|
<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>
|
||||||
<DropdownMenuItem onClick={() => setEditOpen(true)} disabled={!canUpdate}>
|
<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>
|
||||||
<DropdownMenuItem onClick={() => setDeleteOpen(true)} disabled={!canDelete}>
|
<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>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function getQuotationColumns({
|
|||||||
id: 'code',
|
id: 'code',
|
||||||
accessorKey: 'code',
|
accessorKey: 'code',
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Quotation' />
|
<DataTableColumnHeader column={column} title='ใบเสนอราคา' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
@@ -41,8 +41,8 @@ export function getQuotationColumns({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Quotation',
|
label: 'ใบเสนอราคา',
|
||||||
placeholder: 'Search quotations...',
|
placeholder: 'ค้นหาใบเสนอราคา...',
|
||||||
variant: 'text' as const,
|
variant: 'text' as const,
|
||||||
icon: Icons.search
|
icon: Icons.search
|
||||||
},
|
},
|
||||||
@@ -69,7 +69,7 @@ export function getQuotationColumns({
|
|||||||
id: 'quotationType',
|
id: 'quotationType',
|
||||||
accessorKey: 'quotationType',
|
accessorKey: 'quotationType',
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Type' />
|
<DataTableColumnHeader column={column} title='ประเภท' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Badge variant='outline'>
|
<Badge variant='outline'>
|
||||||
@@ -77,7 +77,7 @@ export function getQuotationColumns({
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Type',
|
label: 'ประเภท',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.quotationTypes.map((item) => ({ value: item.id, label: item.label }))
|
options: referenceData.quotationTypes.map((item) => ({ value: item.id, label: item.label }))
|
||||||
},
|
},
|
||||||
@@ -87,13 +87,13 @@ export function getQuotationColumns({
|
|||||||
id: 'branch',
|
id: 'branch',
|
||||||
accessorFn: (row) => row.branchId ?? '',
|
accessorFn: (row) => row.branchId ?? '',
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Branch' />
|
<DataTableColumnHeader column={column} title='สาขา' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>{row.original.branchId ? (branchMap.get(row.original.branchId)?.name ?? 'Unknown') : 'Unassigned'}</span>
|
<span>{row.original.branchId ? (branchMap.get(row.original.branchId)?.name ?? 'Unknown') : 'Unassigned'}</span>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Branch',
|
label: 'สาขา',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.branches.map((item) => ({ value: item.id, label: item.name }))
|
options: referenceData.branches.map((item) => ({ value: item.id, label: item.name }))
|
||||||
},
|
},
|
||||||
@@ -103,11 +103,11 @@ export function getQuotationColumns({
|
|||||||
id: 'customer',
|
id: 'customer',
|
||||||
accessorFn: (row) => row.customerId,
|
accessorFn: (row) => row.customerId,
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Customer' />
|
<DataTableColumnHeader column={column} title='ลูกค้าผู้รับใบเสนอราคา' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.customerName,
|
cell: ({ row }) => row.original.customerName,
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Customer',
|
label: 'ลูกค้าผู้รับใบเสนอราคา',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.customers.map((item) => ({ value: item.id, label: item.name }))
|
options: referenceData.customers.map((item) => ({ value: item.id, label: item.name }))
|
||||||
},
|
},
|
||||||
@@ -117,11 +117,11 @@ export function getQuotationColumns({
|
|||||||
id: 'enquiry',
|
id: 'enquiry',
|
||||||
accessorFn: (row) => row.enquiryId ?? '',
|
accessorFn: (row) => row.enquiryId ?? '',
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Enquiry' />
|
<DataTableColumnHeader column={column} title='โอกาสขาย' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.enquiryCode ?? '-',
|
cell: ({ row }) => row.original.enquiryCode ?? '-',
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Enquiry',
|
label: 'โอกาสขาย',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: referenceData.enquiries.map((item) => ({ value: item.id, label: `${item.code} - ${item.title}` }))
|
options: referenceData.enquiries.map((item) => ({ value: item.id, label: `${item.code} - ${item.title}` }))
|
||||||
},
|
},
|
||||||
@@ -131,15 +131,15 @@ export function getQuotationColumns({
|
|||||||
id: 'isHotProject',
|
id: 'isHotProject',
|
||||||
accessorFn: (row) => String(row.isHotProject),
|
accessorFn: (row) => String(row.isHotProject),
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
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>),
|
cell: ({ row }) => (row.original.isHotProject ? <Badge>Hot</Badge> : <span>-</span>),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Hot Project',
|
label: 'โครงการด่วน',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: [
|
options: [
|
||||||
{ value: 'true', label: 'Hot' },
|
{ value: 'true', label: 'ด่วน' },
|
||||||
{ value: 'false', label: 'Normal' }
|
{ value: 'false', label: 'ปกติ' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
enableColumnFilter: true
|
enableColumnFilter: true
|
||||||
@@ -148,7 +148,7 @@ export function getQuotationColumns({
|
|||||||
id: 'totalAmount',
|
id: 'totalAmount',
|
||||||
accessorKey: 'totalAmount',
|
accessorKey: 'totalAmount',
|
||||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Total' />
|
<DataTableColumnHeader column={column} title='มูลค่ารวม' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.totalAmount.toLocaleString()
|
cell: ({ row }) => row.original.totalAmount.toLocaleString()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1324,7 +1324,7 @@ export function QuotationDetail({
|
|||||||
<TabsList className="flex flex-wrap">
|
<TabsList className="flex flex-wrap">
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<TabsTrigger value="items">Items</TabsTrigger>
|
<TabsTrigger value="items">Items</TabsTrigger>
|
||||||
<TabsTrigger value="customers">Project Parties</TabsTrigger>
|
<TabsTrigger value="customers">ผู้เกี่ยวข้องในโครงการ</TabsTrigger>
|
||||||
<TabsTrigger value="topics">Topics</TabsTrigger>
|
<TabsTrigger value="topics">Topics</TabsTrigger>
|
||||||
<TabsTrigger value="followups">Follow-ups</TabsTrigger>
|
<TabsTrigger value="followups">Follow-ups</TabsTrigger>
|
||||||
<TabsTrigger value="attachments">Attachments</TabsTrigger>
|
<TabsTrigger value="attachments">Attachments</TabsTrigger>
|
||||||
@@ -1343,7 +1343,7 @@ export function QuotationDetail({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label="Billing Customer"
|
label="ลูกค้าผู้รับใบเสนอราคา"
|
||||||
value={customer?.name}
|
value={customer?.name}
|
||||||
/>
|
/>
|
||||||
<FieldItem label="Contact" value={contact?.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="md:col-span-2 xl:col-span-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-muted-foreground text-xs">
|
<div className="text-muted-foreground text-xs">
|
||||||
Project Parties
|
ผู้เกี่ยวข้องในโครงการ
|
||||||
</div>
|
</div>
|
||||||
{!customersData.items.length ? (
|
{!customersData.items.length ? (
|
||||||
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
|
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
|
||||||
@@ -1534,7 +1534,7 @@ export function QuotationDetail({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Project Parties</CardTitle>
|
<CardTitle>ผู้เกี่ยวข้องในโครงการ</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Related companies for this quotation, each with a
|
Related companies for this quotation, each with a
|
||||||
visible role.
|
visible role.
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
const { data } = useSuspenseQuery(quotationDocumentPreviewOptions(quotationId));
|
const { data } = useSuspenseQuery(quotationDocumentPreviewOptions(quotationId));
|
||||||
const { documentData, template } = data.preview;
|
const { documentData, template } = data.preview;
|
||||||
const topicGroups: Array<{ key: 'scope' | 'exclusion' | 'payment'; title: string }> = [
|
const topicGroups: Array<{ key: 'scope' | 'exclusion' | 'payment'; title: string }> = [
|
||||||
{ key: 'scope', title: 'Scope' },
|
{ key: 'scope', title: 'ขอบเขตงาน' },
|
||||||
{ key: 'exclusion', title: 'Exclusions' },
|
{ key: 'exclusion', title: 'ข้อยกเว้น' },
|
||||||
{ key: 'payment', title: 'Payment Terms' }
|
{ key: 'payment', title: 'เงื่อนไขการชำระเงิน' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,14 +32,14 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<CardTitle className='text-2xl'>{documentData.company.name}</CardTitle>
|
<CardTitle className='text-2xl'>{documentData.company.name}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Quotation Preview using {template.template.templateName} v{template.version.version}
|
ตัวอย่างใบเสนอราคา จากเทมเพลต {template.template.templateName} v{template.version.version}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{documentData.watermarkStatus ? (
|
{documentData.watermarkStatus ? (
|
||||||
<Badge variant='destructive'>{documentData.watermarkStatus}</Badge>
|
<Badge variant='destructive'>{documentData.watermarkStatus}</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge>Approved Ready</Badge>
|
<Badge>พร้อมใช้งานหลังอนุมัติ</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge variant='outline'>{template.template.fileType}</Badge>
|
<Badge variant='outline'>{template.template.fileType}</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,41 +49,41 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
<div className='grid gap-6 lg:grid-cols-2'>
|
<div className='grid gap-6 lg:grid-cols-2'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Quotation Header</CardTitle>
|
<CardTitle>ข้อมูลหัวเอกสาร</CardTitle>
|
||||||
<CardDescription>Commercial identity and issue information.</CardDescription>
|
<CardDescription>ข้อมูลอ้างอิงและวันออกเอกสารสำหรับใบเสนอราคา</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2'>
|
<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='Revision' value={documentData.quotation.revisionLabel} />
|
||||||
<Field
|
<Field
|
||||||
label='Quotation Date'
|
label='วันที่ออกใบเสนอราคา'
|
||||||
value={new Date(documentData.quotation.quotationDate).toLocaleDateString()}
|
value={new Date(documentData.quotation.quotationDate).toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
label='Valid Until'
|
label='ใช้ได้ถึง'
|
||||||
value={
|
value={
|
||||||
documentData.quotation.validUntil
|
documentData.quotation.validUntil
|
||||||
? new Date(documentData.quotation.validUntil).toLocaleDateString()
|
? new Date(documentData.quotation.validUntil).toLocaleDateString()
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Field label='Type' value={documentData.quotation.quotationTypeLabel} />
|
<Field label='ประเภทเอกสาร' value={documentData.quotation.quotationTypeLabel} />
|
||||||
<Field label='Branch' value={documentData.branch?.label} />
|
<Field label='สาขา' value={documentData.branch?.label} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Customer Block</CardTitle>
|
<CardTitle>ข้อมูลลูกค้า</CardTitle>
|
||||||
<CardDescription>Bill-to and attention context for the document.</CardDescription>
|
<CardDescription>ข้อมูลลูกค้าผู้รับใบเสนอราคาและผู้ติดต่อของเอกสาร</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2'>
|
<CardContent className='grid gap-4 md:grid-cols-2'>
|
||||||
<Field label='Billing Customer' value={documentData.customer.name} />
|
<Field label='ลูกค้าผู้รับใบเสนอราคา' value={documentData.customer.name} />
|
||||||
<Field label='Contact' value={documentData.contact?.name} />
|
<Field label='ผู้ติดต่อ' value={documentData.contact?.name} />
|
||||||
<Field label='Phone' value={documentData.customer.phone} />
|
<Field label='โทรศัพท์' value={documentData.customer.phone} />
|
||||||
<Field label='Email' value={documentData.customer.email} />
|
<Field label='อีเมล' value={documentData.customer.email} />
|
||||||
<div className='md:col-span-2'>
|
<div className='md:col-span-2'>
|
||||||
<Field label='Address' value={documentData.customer.address} />
|
<Field label='ที่อยู่' value={documentData.customer.address} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -91,30 +91,30 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Project Information</CardTitle>
|
<CardTitle>ข้อมูลโครงการ</CardTitle>
|
||||||
<CardDescription>Scope summary and reference data for the customer.</CardDescription>
|
<CardDescription>สรุปข้อมูลอ้างอิงของโครงการสำหรับลูกค้า</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
<Field label='Project Name' value={documentData.quotation.projectName} />
|
<Field label='ชื่อโครงการ' value={documentData.quotation.projectName} />
|
||||||
<Field label='Site Location' value={documentData.quotation.projectLocation} />
|
<Field label='สถานที่ติดตั้ง' value={documentData.quotation.projectLocation} />
|
||||||
<Field label='Attention' value={documentData.quotation.attention} />
|
<Field label='เรียน' value={documentData.quotation.attention} />
|
||||||
<Field label='Reference' value={documentData.quotation.reference} />
|
<Field label='อ้างอิง' value={documentData.quotation.reference} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Items Table</CardTitle>
|
<CardTitle>ตารางรายการ</CardTitle>
|
||||||
<CardDescription>Normalized line items prepared for template mapping.</CardDescription>
|
<CardDescription>รายการที่จัดรูปแบบแล้วเพื่อเตรียมใช้กับ template mapping</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-3'>
|
<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 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>ลำดับ</div>
|
||||||
<div>Description</div>
|
<div>รายละเอียด</div>
|
||||||
<div>Qty</div>
|
<div>จำนวน</div>
|
||||||
<div>Unit</div>
|
<div>หน่วย</div>
|
||||||
<div>Unit Price</div>
|
<div>ราคาต่อหน่วย</div>
|
||||||
<div>Total</div>
|
<div>รวม</div>
|
||||||
</div>
|
</div>
|
||||||
{documentData.items.map((item) => (
|
{documentData.items.map((item) => (
|
||||||
<div
|
<div
|
||||||
@@ -138,8 +138,8 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
<div className='grid gap-6 lg:grid-cols-[2fr_1fr]'>
|
<div className='grid gap-6 lg:grid-cols-[2fr_1fr]'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Topics</CardTitle>
|
<CardTitle>หัวข้อเอกสาร</CardTitle>
|
||||||
<CardDescription>Scope, exclusions, and payment terms from topic records.</CardDescription>
|
<CardDescription>ขอบเขตงาน ข้อยกเว้น และเงื่อนไขการชำระเงินจากหัวข้อในระบบ</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
{topicGroups.map((group) => {
|
{topicGroups.map((group) => {
|
||||||
@@ -148,7 +148,7 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
<div key={group.key} className='space-y-3'>
|
<div key={group.key} className='space-y-3'>
|
||||||
<div className='font-medium'>{group.title}</div>
|
<div className='font-medium'>{group.title}</div>
|
||||||
{!entries.length ? (
|
{!entries.length ? (
|
||||||
<div className='text-muted-foreground text-sm'>No entries</div>
|
<div className='text-muted-foreground text-sm'>ไม่มีข้อมูล</div>
|
||||||
) : (
|
) : (
|
||||||
entries.map((entry) => (
|
entries.map((entry) => (
|
||||||
<div key={`${group.key}-${entry.title}`} className='rounded-lg border p-4'>
|
<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'>
|
<div className='space-y-6'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Price Summary</CardTitle>
|
<CardTitle>สรุปมูลค่า</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-3 text-sm'>
|
<CardContent className='space-y-3 text-sm'>
|
||||||
<div className='flex items-center justify-between'>
|
<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>
|
<span>{documentData.quotation.subtotal.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<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>
|
<span>{documentData.quotation.taxAmount.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<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>
|
<span className='font-semibold'>{documentData.quotation.totalAmount.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -190,24 +190,24 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Approval Block</CardTitle>
|
<CardTitle>ข้อมูลอนุมัติเอกสาร</CardTitle>
|
||||||
<CardDescription>Prepared for watermark and approval snapshot flows.</CardDescription>
|
<CardDescription>ข้อมูลที่ใช้ประกอบ watermark และ approved snapshot</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<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='Workflow' value={documentData.approval.workflowName} />
|
||||||
<Field label='Approved At' value={documentData.approval.approvedAt} />
|
<Field label='อนุมัติเมื่อ' value={documentData.approval.approvedAt} />
|
||||||
<Separator />
|
<Separator />
|
||||||
{!documentData.approval.approvers.length ? (
|
{!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) => (
|
documentData.approval.approvers.map((item) => (
|
||||||
<div key={`${item.stepNumber}-${item.roleCode}`} className='rounded-lg border p-3 text-sm'>
|
<div key={`${item.stepNumber}-${item.roleCode}`} className='rounded-lg border p-3 text-sm'>
|
||||||
<div className='font-medium'>
|
<div className='font-medium'>
|
||||||
Step {item.stepNumber} - {item.roleName}
|
ขั้น {item.stepNumber} - {item.roleName}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -217,17 +217,17 @@ export function QuotationDocumentPreview({ quotationId }: { quotationId: string
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Signature Placeholders</CardTitle>
|
<CardTitle>ตำแหน่งลายเซ็น</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-3 text-sm'>
|
<CardContent className='space-y-3 text-sm'>
|
||||||
<Field label='Prepared By' value={documentData.signatures.preparedBy} />
|
<Field label='ผู้จัดทำ' value={documentData.signatures.preparedBy} />
|
||||||
<Field label='Requested By' value={documentData.signatures.requestedBy} />
|
<Field label='ผู้ขออนุมัติ' value={documentData.signatures.requestedBy} />
|
||||||
<Field label='Sales Manager' value={documentData.signatures.salesManager} />
|
<Field label='ผู้จัดการฝ่ายขาย' value={documentData.signatures.salesManager} />
|
||||||
<Field
|
<Field
|
||||||
label='Department Manager'
|
label='ผู้จัดการฝ่าย'
|
||||||
value={documentData.signatures.departmentManager}
|
value={documentData.signatures.departmentManager}
|
||||||
/>
|
/>
|
||||||
<Field label='Top Manager' value={documentData.signatures.topManager} />
|
<Field label='ผู้บริหารสูงสุด' value={documentData.signatures.topManager} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export function QuotationFormSheet({
|
|||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label='Billing Customer'>
|
<Field label='ลูกค้าผู้รับใบเสนอราคา'>
|
||||||
<Select
|
<Select
|
||||||
value={state.customerId}
|
value={state.customerId}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getProjectPartyRoleThaiLabel } from './terminology';
|
||||||
|
|
||||||
export const PROJECT_PARTY_OPTION_CATEGORY = 'crm_project_party_role';
|
export const PROJECT_PARTY_OPTION_CATEGORY = 'crm_project_party_role';
|
||||||
|
|
||||||
const LEGACY_PROJECT_PARTY_ROLE_CODE_MAP = {
|
const LEGACY_PROJECT_PARTY_ROLE_CODE_MAP = {
|
||||||
@@ -8,12 +10,12 @@ const LEGACY_PROJECT_PARTY_ROLE_CODE_MAP = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROJECT_PARTY_CODE_LABELS: Record<string, string> = {
|
export const PROJECT_PARTY_CODE_LABELS: Record<string, string> = {
|
||||||
billing_customer: 'Billing Customer',
|
billing_customer: getProjectPartyRoleThaiLabel('billing_customer'),
|
||||||
customer: 'Customer',
|
customer: getProjectPartyRoleThaiLabel('customer'),
|
||||||
consultant: 'Consultant',
|
consultant: getProjectPartyRoleThaiLabel('consultant'),
|
||||||
contractor: 'Contractor',
|
contractor: getProjectPartyRoleThaiLabel('contractor'),
|
||||||
end_customer: 'End Customer',
|
end_customer: getProjectPartyRoleThaiLabel('end_customer'),
|
||||||
other: 'Other'
|
other: getProjectPartyRoleThaiLabel('other')
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ProjectPartyRoleOptionLike {
|
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;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
id: 'entityCode',
|
id: 'entityCode',
|
||||||
accessorFn: (row) => row.entityCode ?? row.entityId,
|
accessorFn: (row) => row.entityCode ?? row.entityId,
|
||||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Request' />
|
<DataTableColumnHeader column={column} title='คำขอ' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
@@ -27,8 +27,8 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Request',
|
label: 'คำขอ',
|
||||||
placeholder: 'Search approvals...',
|
placeholder: 'ค้นหางานอนุมัติ...',
|
||||||
variant: 'text' as const,
|
variant: 'text' as const,
|
||||||
icon: Icons.search
|
icon: Icons.search
|
||||||
},
|
},
|
||||||
@@ -45,11 +45,11 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: [
|
options: [
|
||||||
{ value: 'pending', label: 'Pending' },
|
{ value: 'pending', label: 'รออนุมัติ' },
|
||||||
{ value: 'approved', label: 'Approved' },
|
{ value: 'approved', label: 'อนุมัติแล้ว' },
|
||||||
{ value: 'rejected', label: 'Rejected' },
|
{ value: 'rejected', label: 'ไม่อนุมัติ' },
|
||||||
{ value: 'returned', label: 'Returned' },
|
{ value: 'returned', label: 'ตีกลับ' },
|
||||||
{ value: 'cancelled', label: 'Cancelled' }
|
{ value: 'cancelled', label: 'ยกเลิก' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
enableColumnFilter: true
|
enableColumnFilter: true
|
||||||
@@ -58,13 +58,13 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
id: 'entityType',
|
id: 'entityType',
|
||||||
accessorKey: 'entityType',
|
accessorKey: 'entityType',
|
||||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Entity' />
|
<DataTableColumnHeader column={column} title='เอกสาร' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.entityType.replaceAll('_', ' '),
|
cell: ({ row }) => row.original.entityType.replaceAll('_', ' '),
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Entity',
|
label: 'เอกสาร',
|
||||||
variant: 'multiSelect' as const,
|
variant: 'multiSelect' as const,
|
||||||
options: [{ value: 'quotation', label: 'Quotation' }]
|
options: [{ value: 'quotation', label: 'ใบเสนอราคา' }]
|
||||||
},
|
},
|
||||||
enableColumnFilter: true
|
enableColumnFilter: true
|
||||||
},
|
},
|
||||||
@@ -72,11 +72,11 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
id: 'currentStep',
|
id: 'currentStep',
|
||||||
accessorKey: 'currentStep',
|
accessorKey: 'currentStep',
|
||||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Current Step' />
|
<DataTableColumnHeader column={column} title='ขั้นตอนปัจจุบัน' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span>Step {row.original.currentStep}</span>
|
<span>ขั้น {row.original.currentStep}</span>
|
||||||
<span className='text-muted-foreground text-xs'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
{row.original.currentStepRoleName ?? '-'}
|
{row.original.currentStepRoleName ?? '-'}
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +87,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
id: 'requestedBy',
|
id: 'requestedBy',
|
||||||
accessorFn: (row) => row.requestedByName ?? row.requestedBy,
|
accessorFn: (row) => row.requestedByName ?? row.requestedBy,
|
||||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Requested By' />
|
<DataTableColumnHeader column={column} title='ผู้ส่งอนุมัติ' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.requestedByName ?? row.original.requestedBy
|
cell: ({ row }) => row.original.requestedByName ?? row.original.requestedBy
|
||||||
},
|
},
|
||||||
@@ -95,7 +95,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
|||||||
id: 'requestedAt',
|
id: 'requestedAt',
|
||||||
accessorKey: 'requestedAt',
|
accessorKey: 'requestedAt',
|
||||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||||
<DataTableColumnHeader column={column} title='Requested At' />
|
<DataTableColumnHeader column={column} title='วันที่ส่งอนุมัติ' />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => new Date(row.original.requestedAt).toLocaleString()
|
cell: ({ row }) => new Date(row.original.requestedAt).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,35 +60,35 @@ export function ApprovalRequestPanel({
|
|||||||
const approveAction = useMutation({
|
const approveAction = useMutation({
|
||||||
...approveApprovalMutation,
|
...approveApprovalMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Approval step completed');
|
toast.success('อนุมัติขั้นตอนนี้สำเร็จ');
|
||||||
setRemark('');
|
setRemark('');
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Unable to approve request')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอนุมัติคำขอได้')
|
||||||
});
|
});
|
||||||
const rejectAction = useMutation({
|
const rejectAction = useMutation({
|
||||||
...rejectApprovalMutation,
|
...rejectApprovalMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Approval request rejected');
|
toast.success('ไม่อนุมัติคำขอสำเร็จ');
|
||||||
setRemark('');
|
setRemark('');
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Unable to reject request')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถไม่อนุมัติคำขอได้')
|
||||||
});
|
});
|
||||||
const returnAction = useMutation({
|
const returnAction = useMutation({
|
||||||
...returnApprovalMutation,
|
...returnApprovalMutation,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Approval request returned to draft');
|
toast.success('ตีกลับคำขอเพื่อแก้ไขสำเร็จ');
|
||||||
setRemark('');
|
setRemark('');
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Unable to return request')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถตีกลับคำขอได้')
|
||||||
});
|
});
|
||||||
const cancelAction = useMutation({
|
const cancelAction = useMutation({
|
||||||
...cancelApprovalMutation,
|
...cancelApprovalMutation,
|
||||||
onSuccess: () => toast.success('Approval request cancelled'),
|
onSuccess: () => toast.success('ยกเลิกคำขออนุมัติสำเร็จ'),
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(error instanceof Error ? error.message : 'Unable to cancel request')
|
toast.error(error instanceof Error ? error.message : 'ไม่สามารถยกเลิกคำขอได้')
|
||||||
});
|
});
|
||||||
const isActing =
|
const isActing =
|
||||||
approveAction.isPending ||
|
approveAction.isPending ||
|
||||||
@@ -100,30 +100,30 @@ export function ApprovalRequestPanel({
|
|||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Request Information</CardTitle>
|
<CardTitle>ข้อมูลคำขอ</CardTitle>
|
||||||
<CardDescription>Current workflow state for this approval request.</CardDescription>
|
<CardDescription>สถานะปัจจุบันของคำขออนุมัติเอกสารนี้</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
<FieldItem label='Workflow' value={approval.workflow.name} />
|
<FieldItem label='Workflow' value={approval.workflow.name} />
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<div className='text-muted-foreground text-xs'>Status</div>
|
<div className='text-muted-foreground text-xs'>สถานะ</div>
|
||||||
<ApprovalStatusBadge status={approval.request.status} />
|
<ApprovalStatusBadge status={approval.request.status} />
|
||||||
</div>
|
</div>
|
||||||
<FieldItem label='Entity Type' value={approval.request.entityType.replaceAll('_', ' ')} />
|
<FieldItem label='ประเภทเอกสาร' value={approval.request.entityType.replaceAll('_', ' ')} />
|
||||||
<FieldItem label='Entity Code' value={approval.entityCode ?? approval.request.entityId} />
|
<FieldItem label='รหัสเอกสาร' value={approval.entityCode ?? approval.request.entityId} />
|
||||||
<FieldItem label='Entity Title' value={approval.entityTitle} />
|
<FieldItem label='ชื่อเอกสาร' value={approval.entityTitle} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Requested At'
|
label='วันที่ส่งอนุมัติ'
|
||||||
value={new Date(approval.request.requestedAt).toLocaleString()}
|
value={new Date(approval.request.requestedAt).toLocaleString()}
|
||||||
/>
|
/>
|
||||||
<FieldItem label='Requested By' value={approval.request.requestedBy} />
|
<FieldItem label='ผู้ส่งอนุมัติ' value={approval.request.requestedBy} />
|
||||||
<FieldItem
|
<FieldItem
|
||||||
label='Current Step'
|
label='ขั้นตอนปัจจุบัน'
|
||||||
value={
|
value={
|
||||||
approval.currentStep
|
approval.currentStep
|
||||||
? `Step ${approval.currentStep.stepNumber} - ${approval.currentStep.roleName}`
|
? `Step ${approval.currentStep.stepNumber} - ${approval.currentStep.roleName}`
|
||||||
: approval.request.status === 'approved'
|
: approval.request.status === 'approved'
|
||||||
? 'Completed'
|
? 'เสร็จสิ้น'
|
||||||
: '-'
|
: '-'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -132,7 +132,7 @@ export function ApprovalRequestPanel({
|
|||||||
<Button asChild variant='outline'>
|
<Button asChild variant='outline'>
|
||||||
<Link href={entityHref}>
|
<Link href={entityHref}>
|
||||||
<Icons.arrowRight className='mr-2 h-4 w-4' />
|
<Icons.arrowRight className='mr-2 h-4 w-4' />
|
||||||
Open Related Document
|
เปิดเอกสารที่เกี่ยวข้อง
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +143,7 @@ export function ApprovalRequestPanel({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Workflow</CardTitle>
|
<CardTitle>Workflow</CardTitle>
|
||||||
<CardDescription>Sequential approver chain configured for this document.</CardDescription>
|
<CardDescription>ลำดับผู้อนุมัติที่กำหนดไว้สำหรับเอกสารนี้</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-3'>
|
<CardContent className='space-y-3'>
|
||||||
{approval.steps.map((step) => {
|
{approval.steps.map((step) => {
|
||||||
@@ -157,12 +157,12 @@ export function ApprovalRequestPanel({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className='font-medium'>
|
<div className='font-medium'>
|
||||||
Step {step.stepNumber} - {step.roleName}
|
ขั้น {step.stepNumber} - {step.roleName}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-sm'>{step.roleCode}</div>
|
<div className='text-muted-foreground text-sm'>{step.roleCode}</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={isCurrent ? 'secondary' : isCompleted ? 'default' : 'outline'}>
|
<Badge variant={isCurrent ? 'secondary' : isCompleted ? 'default' : 'outline'}>
|
||||||
{isCurrent ? 'Current' : isCompleted ? 'Completed' : 'Pending'}
|
{isCurrent ? 'ปัจจุบัน' : isCompleted ? 'เสร็จสิ้น' : 'รอดำเนินการ'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -172,16 +172,16 @@ export function ApprovalRequestPanel({
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Actions</CardTitle>
|
<CardTitle>การดำเนินการ</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Approvers can approve, reject, or return on their assigned step.
|
ผู้อนุมัติสามารถอนุมัติ ไม่อนุมัติ หรือตีกลับในขั้นที่รับผิดชอบได้
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={remark}
|
value={remark}
|
||||||
onChange={(event) => setRemark(event.target.value)}
|
onChange={(event) => setRemark(event.target.value)}
|
||||||
placeholder='Optional remark for approval history'
|
placeholder='หมายเหตุเพิ่มเติมสำหรับประวัติการอนุมัติ'
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
@@ -193,7 +193,7 @@ export function ApprovalRequestPanel({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icons.circleCheck className='mr-2 h-4 w-4' />
|
<Icons.circleCheck className='mr-2 h-4 w-4' />
|
||||||
Approve
|
อนุมัติ
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{canReject && canHandleCurrentStep && isPending ? (
|
{canReject && canHandleCurrentStep && isPending ? (
|
||||||
@@ -205,7 +205,7 @@ export function ApprovalRequestPanel({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icons.warning className='mr-2 h-4 w-4' />
|
<Icons.warning className='mr-2 h-4 w-4' />
|
||||||
Reject
|
ไม่อนุมัติ
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{canReturn && canHandleCurrentStep && isPending ? (
|
{canReturn && canHandleCurrentStep && isPending ? (
|
||||||
@@ -217,7 +217,7 @@ export function ApprovalRequestPanel({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icons.chevronLeft className='mr-2 h-4 w-4' />
|
<Icons.chevronLeft className='mr-2 h-4 w-4' />
|
||||||
Return
|
ตีกลับ
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{canCancelCurrentRequest ? (
|
{canCancelCurrentRequest ? (
|
||||||
@@ -227,36 +227,36 @@ export function ApprovalRequestPanel({
|
|||||||
onClick={() => cancelAction.mutate(approval.request.id)}
|
onClick={() => cancelAction.mutate(approval.request.id)}
|
||||||
>
|
>
|
||||||
<Icons.close className='mr-2 h-4 w-4' />
|
<Icons.close className='mr-2 h-4 w-4' />
|
||||||
Cancel Request
|
ยกเลิกคำขอ
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{!canHandleCurrentStep && isPending ? (
|
{!canHandleCurrentStep && isPending ? (
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
Current step is assigned to{' '}
|
ขั้นตอนปัจจุบันถูกมอบหมายให้{' '}
|
||||||
<span className='font-medium'>{approval.currentStep?.roleName ?? 'another role'}</span>.
|
<span className='font-medium'>{approval.currentStep?.roleName ?? 'another role'}</span>.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!isPending ? (
|
{!isPending ? (
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
This request is already {approval.request.status.replaceAll('_', ' ')}.
|
คำขอนี้อยู่ในสถานะ {approval.request.status.replaceAll('_', ' ')} แล้ว
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{isActing ? (
|
{isActing ? (
|
||||||
<div className='text-muted-foreground text-xs'>Saving approval action...</div>
|
<div className='text-muted-foreground text-xs'>กำลังบันทึกการอนุมัติ...</div>
|
||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Timeline</CardTitle>
|
<CardTitle>ไทม์ไลน์</CardTitle>
|
||||||
<CardDescription>Full audit trail of request submission and approval actions.</CardDescription>
|
<CardDescription>ประวัติการส่งคำขอและการดำเนินการอนุมัติทั้งหมด</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
{!approval.timeline.length ? (
|
{!approval.timeline.length ? (
|
||||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||||
No approval actions recorded yet.
|
ยังไม่มีประวัติการอนุมัติ
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
approval.timeline.map((item, index) => (
|
approval.timeline.map((item, index) => (
|
||||||
@@ -272,7 +272,7 @@ export function ApprovalRequestPanel({
|
|||||||
{item.actorName ?? item.actedBy}
|
{item.actorName ?? item.actedBy}
|
||||||
</span>
|
</span>
|
||||||
<span className='text-muted-foreground text-xs'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
Step {item.stepNumber}
|
ขั้น {item.stepNumber}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { Icons } from '@/components/icons';
|
|||||||
|
|
||||||
export function ApprovalStatusBadge({ status }: { status: string }) {
|
export function ApprovalStatusBadge({ status }: { status: string }) {
|
||||||
const normalized = status.toLowerCase();
|
const normalized = status.toLowerCase();
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
pending: 'รออนุมัติ',
|
||||||
|
approved: 'อนุมัติแล้ว',
|
||||||
|
rejected: 'ไม่อนุมัติ',
|
||||||
|
returned: 'ตีกลับ',
|
||||||
|
cancelled: 'ยกเลิก'
|
||||||
|
};
|
||||||
const variant =
|
const variant =
|
||||||
normalized === 'approved'
|
normalized === 'approved'
|
||||||
? 'default'
|
? 'default'
|
||||||
@@ -25,7 +32,7 @@ export function ApprovalStatusBadge({ status }: { status: string }) {
|
|||||||
return (
|
return (
|
||||||
<Badge variant={variant} className='capitalize'>
|
<Badge variant={variant} className='capitalize'>
|
||||||
<Icon className='h-3 w-3' />
|
<Icon className='h-3 w-3' />
|
||||||
{status.replaceAll('_', ' ')}
|
{labelMap[normalized] ?? status.replaceAll('_', ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user