From b8f13e36b3e9b63764dc7a8cec45e6b05873a121 Mon Sep 17 00:00:00 2001 From: phaichayon Date: Tue, 16 Jun 2026 08:43:19 +0700 Subject: [PATCH] task-e --- docs/adr/0007-quotation-revision-strategy.md | 18 ++ docs/adr/0008-attachment-storage-strategy.md | 15 ++ .../task-e1-quotation-stabilization.md | 96 +++++++++ docs/implementation/technical-debt.md | 25 +++ plans/task-e.1.md | 202 ++++++++++++++++++ src/db/seeds/foundation.seed.ts | 31 ++- src/features/crm/enquiries/server/service.ts | 2 - src/features/crm/quotations/api/types.ts | 3 + .../components/quotation-detail.tsx | 69 +++++- .../components/quotation-form-sheet.tsx | 19 ++ src/features/crm/quotations/server/service.ts | 82 ++++++- .../foundation/master-options/types.ts | 6 + 12 files changed, 547 insertions(+), 21 deletions(-) create mode 100644 docs/adr/0007-quotation-revision-strategy.md create mode 100644 docs/adr/0008-attachment-storage-strategy.md create mode 100644 docs/implementation/task-e1-quotation-stabilization.md create mode 100644 plans/task-e.1.md diff --git a/docs/adr/0007-quotation-revision-strategy.md b/docs/adr/0007-quotation-revision-strategy.md new file mode 100644 index 0000000..bee1e24 --- /dev/null +++ b/docs/adr/0007-quotation-revision-strategy.md @@ -0,0 +1,18 @@ +Status: +accepted + +Context: +Quotation revision was introduced in Task E, but the rules before approval workflow were still ambiguous. +Task E.1 needs a stable rule so Task F can build approval on top without changing quotation lineage again. + +Decision: +- Revision is allowed only when the current quotation status is `approved`, `sent`, `accepted`, or `rejected`. +- Revision is not allowed for `draft` or `cancelled`. +- A new revision keeps the same quotation family using `parentQuotationId`. +- A new revision copies header, items, customers, and topics only. +- A new revision is created with the `revised` status when that master option exists. + +Consequences: +- Sales can revise commercially meaningful documents without reopening early drafts. +- Approval work can later key off a stable quotation family and revision chain. +- Follow-ups and attachments are still out of the revision snapshot and remain technical debt until explicitly designed. diff --git a/docs/adr/0008-attachment-storage-strategy.md b/docs/adr/0008-attachment-storage-strategy.md new file mode 100644 index 0000000..ff47bc5 --- /dev/null +++ b/docs/adr/0008-attachment-storage-strategy.md @@ -0,0 +1,15 @@ +Status: +accepted + +Context: +Task E introduced quotation attachments as metadata only. +Task E.1 confirms that approval should not start on top of an unstable file-storage design. + +Decision: +- Keep quotation attachments metadata-only for now. +- Store file identity, original name, path, type, size, uploader, and timestamps in CRM tables. +- Do not build upload transport, storage provider logic, signed download URLs, or binary retention policy in Task E or E.1. + +Consequences: +- Task F can reference attachment metadata without being blocked by storage implementation details. +- A later storage task must define provider abstraction, permission checks, retention rules, and migration strategy for existing metadata rows. diff --git a/docs/implementation/task-e1-quotation-stabilization.md b/docs/implementation/task-e1-quotation-stabilization.md new file mode 100644 index 0000000..fb85c51 --- /dev/null +++ b/docs/implementation/task-e1-quotation-stabilization.md @@ -0,0 +1,96 @@ +# Task E.1: Quotation Stabilization Before Approval + +## 1. Files Added + +- `docs/adr/0007-quotation-revision-strategy.md` +- `docs/adr/0008-attachment-storage-strategy.md` + +## 2. Files Modified + +- `src/db/seeds/foundation.seed.ts` +- `src/features/foundation/master-options/types.ts` +- `src/features/crm/quotations/api/types.ts` +- `src/features/crm/quotations/server/service.ts` +- `src/features/crm/quotations/components/quotation-form-sheet.tsx` +- `src/features/crm/quotations/components/quotation-detail.tsx` +- `src/features/crm/enquiries/server/service.ts` +- `docs/implementation/technical-debt.md` + +## 3. Options Seeded + +- `crm_quotation_type` + - `crane` + - `dockdoor` + - `solarcell` + - `service` + - `other` +- `crm_discount_type` + - `fixed` + - `percentage` +- `crm_unit` + - `pcs` + - `set` + - `lot` + - `job` +- `crm_sent_via` + - `email` + - `manual` + - `system` +- `crm_quotation_customer_role` + - `owner` + - `consultant` + - `contractor` + - `billing` +- `crm_quotation_topic_type` + - `scope` + - `exclusion` + - `payment` + +## 4. Revision Rule Confirmed + +- `draft`: cannot revise +- `cancelled`: cannot revise +- `accepted`: can revise +- `rejected`: can revise +- `sent`: can revise +- `approved`: can revise + +Implementation notes: + +- revision guard now runs in quotation server service before a new revision is created +- revision readiness is based on quotation status master-option code, not on UI-only conditions +- new revision rows attempt to start with `revised` status when that option exists + +## 5. Approval Readiness + +- quotation status seed now explicitly supports: + - `draft` + - `pending_approval` + - `approved` + - `rejected` + - `sent` + - `accepted` + - `lost` + - `cancelled` + - `revised` +- quotation detail now exposes a placeholder `Submit for approval` action for `draft` and `revised` quotations +- no approval table or approval workflow was added in Task E.1 + +## 6. Remaining Risks + +- quotation summary still does not support mixed tax rollup from per-line tax strategies +- attachments are still metadata-only and not backed by upload/storage plumbing +- revision snapshot still excludes follow-ups and attachments +- CRM entity relationships still rely on organization-scoped validation instead of database foreign keys +- approval workflow is still pending and starts in Task F + +## 7. Task F Readiness + +- quotation forms now read status, quotation type, currency, discount type, unit, sent via, customer role, and topic type from master options instead of hardcoded UI choices where practical +- quotation revision eligibility is explicit and server-enforced +- approval-ready status vocabulary exists before approval workflow work begins +- ADR notes now capture the revision and attachment strategies so Task F can build on stable assumptions + +## 8. Verification + +- `npx tsc --noEmit` diff --git a/docs/implementation/technical-debt.md b/docs/implementation/technical-debt.md index 8994721..50e6258 100644 --- a/docs/implementation/technical-debt.md +++ b/docs/implementation/technical-debt.md @@ -115,3 +115,28 @@ Future: Priority: * High + +### 9. Approval Workflow Pending + +Quotation stabilization in Task E.1 only prepares: + +* approval-ready statuses +* revision guard +* placeholder submit-for-approval action + +Current: + +* no approval table +* no approval requester/approver history +* no multi-step approval rule +* no approval notification + +Future: + +* introduce approval aggregate in Task F +* separate workflow state from quotation commercial state where needed +* define approver resolution by branch / amount / role + +Priority: + +* High diff --git a/plans/task-e.1.md b/plans/task-e.1.md new file mode 100644 index 0000000..9e174f7 --- /dev/null +++ b/plans/task-e.1.md @@ -0,0 +1,202 @@ +# Task E.1: Quotation Stabilization Before Approval + +## Goal + +Stabilize Quotation foundation before starting Task F: Approval Production Module. + +## Must Read + +```txt +docs/implementation/task-a-template-audit.md +docs/implementation/task-b-template-audit.md +docs/implementation/task-b1-foundation-stabilization.md +docs/implementation/technical-debt.md +src/features/crm/quotations/** +src/features/foundation/** +src/db/schema.ts +``` + +## Scope + +ทำเฉพาะ stabilization: + +1. Review quotation master options +2. Seed missing options +3. Review quotation revision rule +4. Review quotation totals rule +5. Review approval readiness +6. Update technical debt / ADR notes if needed + +## Part 1: Master Options Seed + +ตรวจสอบและ seed เพิ่มถ้ายังไม่มี: + +```txt +crm_quotation_type +crm_discount_type +crm_unit +crm_sent_via +crm_quotation_customer_role +crm_quotation_topic_type +``` + +ตัวอย่างค่า: + +```txt +crm_quotation_type: +- crane +- dockdoor +- solarcell +- service +- other + +crm_discount_type: +- percentage +- fixed + +crm_unit: +- pcs +- set +- lot +- job + +crm_sent_via: +- email +- manual +- system + +crm_quotation_customer_role: +- owner +- consultant +- contractor +- billing + +crm_quotation_topic_type: +- scope +- exclusion +- payment +``` + +Rules: + +* seed idempotent +* ห้าม hardcode organizationId +* update `foundation.seed.ts` เท่านั้นถ้าเหมาะสม + +## Part 2: Form Option Review + +ตรวจสอบ quotation form ว่า: + +* status มาจาก master options +* quotationType มาจาก master options +* currency มาจาก master options +* discountType มาจาก master options +* unit มาจาก master options ถ้ามี item form +* customer role/topic type ไม่ hardcode ถ้าดึง option ได้ง่าย + +## Part 3: Revision Rule Review + +ตรวจสอบและสรุป rule: + +```txt +draft revise ได้ไหม +cancelled revise ได้ไหม +accepted revise ได้ไหม +rejected revise ได้ไหม +``` + +Recommendation: + +```txt +draft: no +cancelled: no +accepted: yes +rejected: yes +sent: yes +approved: yes +``` + +ถ้า code ยังไม่ตรง ให้แก้เฉพาะ revision guard + +## Part 4: Approval Readiness + +ตรวจสอบว่า quotation status รองรับ: + +```txt +draft +pending_approval +approved +rejected +sent +accepted +lost +cancelled +revised +``` + +ยังไม่ต้องสร้าง approval table + +ให้เพิ่ม placeholder action ได้เท่านั้นถ้าจำเป็น: + +```txt +Submit for approval +``` + +แต่ action นี้ยังไม่ต้องทำ workflow จริง + +## Part 5: Documentation + +อัปเดต: + +```txt +docs/implementation/technical-debt.md +``` + +เพิ่ม/ยืนยันหัวข้อ: + +* mixed tax rollup +* attachment storage +* revision snapshot +* CRM foreign keys +* approval workflow pending + +ถ้ามี ADR folder ให้สร้าง note ได้: + +```txt +docs/adr/0007-quotation-revision-strategy.md +docs/adr/0008-attachment-storage-strategy.md +``` + +## ห้ามทำ + +```txt +Approval table +Approval workflow +PDF generation +Dashboard KPI +Report +Notification +``` + +## Output + +สรุป: + +1. Files Added +2. Files Modified +3. Options Seeded +4. Revision Rule Confirmed +5. Approval Readiness +6. Remaining Risks +7. Task F Readiness + +## Definition of Done + +Task E.1 ผ่านเมื่อ: + +* quotation options seed ครบ +* quotation form ไม่ hardcode option สำคัญ +* revision guard ชัดเจน +* technical debt updated +* approval readiness confirmed +* ยังไม่สร้าง approval workflow จริง diff --git a/src/db/seeds/foundation.seed.ts b/src/db/seeds/foundation.seed.ts index 8861de6..a04b7ac 100644 --- a/src/db/seeds/foundation.seed.ts +++ b/src/db/seeds/foundation.seed.ts @@ -122,9 +122,11 @@ const FOUNDATION_OPTIONS = { { code: 'revised', label: 'Revised', value: 'revised', sortOrder: 9 } ], crm_quotation_type: [ - { code: 'standard', label: 'Standard', value: 'standard', sortOrder: 1 }, - { code: 'budgetary', label: 'Budgetary', value: 'budgetary', sortOrder: 2 }, - { code: 'service', label: 'Service', value: 'service', sortOrder: 3 } + { code: 'crane', label: 'Crane', value: 'crane', sortOrder: 1 }, + { code: 'dockdoor', label: 'Dock Door', value: 'dockdoor', sortOrder: 2 }, + { code: 'solarcell', label: 'Solar Cell', value: 'solarcell', sortOrder: 3 }, + { code: 'service', label: 'Service', value: 'service', sortOrder: 4 }, + { code: 'other', label: 'Other', value: 'other', sortOrder: 5 } ], crm_product_type: [ { code: 'crane', label: 'Crane', value: 'crane', sortOrder: 1 }, @@ -137,10 +139,27 @@ const FOUNDATION_OPTIONS = { { code: 'EUR', label: 'EUR', value: 'EUR', sortOrder: 3 } ], crm_discount_type: [ - { code: 'amount', label: 'Amount', value: 'amount', sortOrder: 1 }, - { code: 'percent', label: 'Percent', value: 'percent', sortOrder: 2 } + { code: 'fixed', label: 'Fixed', value: 'fixed', sortOrder: 1 }, + { code: 'percentage', label: 'Percentage', value: 'percentage', sortOrder: 2 } ], - crm_topic_type: [ + crm_unit: [ + { code: 'pcs', label: 'PCS', value: 'pcs', sortOrder: 1 }, + { code: 'set', label: 'Set', value: 'set', sortOrder: 2 }, + { code: 'lot', label: 'Lot', value: 'lot', sortOrder: 3 }, + { code: 'job', label: 'Job', value: 'job', sortOrder: 4 } + ], + crm_sent_via: [ + { code: 'email', label: 'Email', value: 'email', sortOrder: 1 }, + { code: 'manual', label: 'Manual', value: 'manual', sortOrder: 2 }, + { code: 'system', label: 'System', value: 'system', sortOrder: 3 } + ], + crm_quotation_customer_role: [ + { code: 'owner', label: 'Owner', value: 'owner', sortOrder: 1 }, + { code: 'consultant', label: 'Consultant', value: 'consultant', sortOrder: 2 }, + { code: 'contractor', label: 'Contractor', value: 'contractor', sortOrder: 3 }, + { code: 'billing', label: 'Billing', value: 'billing', sortOrder: 4 } + ], + crm_quotation_topic_type: [ { code: 'scope', label: 'Scope', value: 'scope', sortOrder: 1 }, { code: 'exclusion', label: 'Exclusion', value: 'exclusion', sortOrder: 2 }, { code: 'payment', label: 'Payment', value: 'payment', sortOrder: 3 } diff --git a/src/features/crm/enquiries/server/service.ts b/src/features/crm/enquiries/server/service.ts index 574e794..4adc61d 100644 --- a/src/features/crm/enquiries/server/service.ts +++ b/src/features/crm/enquiries/server/service.ts @@ -15,8 +15,6 @@ import { getActiveOptionsByCategory } from '@/features/foundation/master-options import type { EnquiryActivityRecord, EnquiryBranchOption, - EnquiryContactLookup, - EnquiryCustomerLookup, EnquiryCustomerRelationItem, EnquiryFilters, EnquiryFollowupMutationPayload, diff --git a/src/features/crm/quotations/api/types.ts b/src/features/crm/quotations/api/types.ts index 4b63529..03a1abf 100644 --- a/src/features/crm/quotations/api/types.ts +++ b/src/features/crm/quotations/api/types.ts @@ -220,6 +220,9 @@ export interface QuotationReferenceData { quotationTypes: QuotationOption[]; currencies: QuotationOption[]; discountTypes: QuotationOption[]; + units: QuotationOption[]; + sentVias: QuotationOption[]; + customerRoles: QuotationOption[]; topicTypes: QuotationOption[]; followupTypes: QuotationOption[]; productTypes: QuotationOption[]; diff --git a/src/features/crm/quotations/components/quotation-detail.tsx b/src/features/crm/quotations/components/quotation-detail.tsx index 0e34c01..c1c0485 100644 --- a/src/features/crm/quotations/components/quotation-detail.tsx +++ b/src/features/crm/quotations/components/quotation-detail.tsx @@ -40,6 +40,7 @@ import { deleteQuotationFollowupMutation, deleteQuotationItemMutation, deleteQuotationTopicMutation, + updateQuotationMutation, updateQuotationAttachmentMutation, updateQuotationCustomerMutation, updateQuotationFollowupMutation, @@ -134,7 +135,15 @@ function ItemDialog({ setValues((s) => ({ ...s, description: e.target.value }))} placeholder='Description' />
setValues((s) => ({ ...s, quantity: e.target.value }))} type='number' placeholder='Quantity' /> - setValues((s) => ({ ...s, unit: e.target.value }))} placeholder='Unit' /> +
setValues((s) => ({ ...s, unitPrice: e.target.value }))} type='number' placeholder='Unit price' /> @@ -219,8 +228,8 @@ function CustomerDialog({ @@ -601,6 +610,12 @@ export function QuotationDetail({ onSuccess: () => toast.success('Revision created successfully'), onError: (error) => toast.error(error instanceof Error ? error.message : 'Failed to create revision') }); + const submitForApproval = useMutation({ + ...updateQuotationMutation, + onSuccess: () => toast.success('Quotation moved to pending approval'), + onError: (error) => + toast.error(error instanceof Error ? error.message : 'Failed to submit for approval') + }); const branchMap = useMemo( () => new Map(referenceData.branches.map((item) => [item.id, item.name])), @@ -637,6 +652,10 @@ export function QuotationDetail({ const customer = customerMap.get(quotation.customerId); const contact = quotation.contactId ? contactMap.get(quotation.contactId) : null; const status = statusMap.get(quotation.status); + const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes( + status?.code ?? '' + ); + const canSubmitApproval = ['draft', 'revised'].includes(status?.code ?? ''); return (
@@ -795,7 +814,49 @@ export function QuotationDetail({
- {canCreateRevision ? ( + {canUpdate && canSubmitApproval ? ( + + ) : null} + {canCreateRevision && canReviseByStatus ? (