hotfix-add subgroup customer
This commit is contained in:
326
plans/hotfix-1-customer.md
Normal file
326
plans/hotfix-1-customer.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Hotfix: Add Customer Sub Group to Customer Form
|
||||
|
||||
## Goal
|
||||
|
||||
เพิ่ม field `Customer Sub Group` ในหน้า Add / Edit Customer
|
||||
|
||||
โดยใช้ master option category:
|
||||
|
||||
```txt
|
||||
crm_customer_sub_group
|
||||
```
|
||||
|
||||
และต้อง filter ตาม `Customer Group` ที่เลือก
|
||||
|
||||
Relationship:
|
||||
|
||||
```txt
|
||||
crm_customer_group
|
||||
↓ parent
|
||||
crm_customer_sub_group
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Must Read
|
||||
|
||||
```txt
|
||||
src/features/crm/customers/**
|
||||
src/features/foundation/master-options/**
|
||||
src/app/api/foundation/master-options/route.ts
|
||||
src/db/schema.ts
|
||||
src/db/seeds/foundation.seed.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope 1: Data Model Check
|
||||
|
||||
ตรวจสอบว่า customer schema มี field สำหรับ sub group แล้วหรือยัง
|
||||
|
||||
Expected field:
|
||||
|
||||
```txt
|
||||
customerSubGroup
|
||||
```
|
||||
|
||||
หรือถ้าใช้ naming เดิม:
|
||||
|
||||
```txt
|
||||
custSubGroup
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* ถ้ามี field อยู่แล้ว ให้ reuse
|
||||
* ถ้ายังไม่มี ให้เพิ่ม field ใหม่แบบไม่กระทบข้อมูลเดิม
|
||||
* field เป็น optional
|
||||
* เก็บเป็น master option code/value ตาม pattern เดิมของ customerGroup
|
||||
|
||||
---
|
||||
|
||||
## Scope 2: Master Option Category
|
||||
|
||||
เพิ่ม/ยืนยัน category:
|
||||
|
||||
```txt
|
||||
crm_customer_sub_group
|
||||
```
|
||||
|
||||
Sub group ต้องมี parent relationship กับ group:
|
||||
|
||||
```txt
|
||||
crm_customer_group.id
|
||||
↓
|
||||
crm_customer_sub_group.parentId
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* parentId ต้องชี้ไป customer group option
|
||||
* sub group dropdown ต้อง filter ด้วย parentId
|
||||
* ถ้ายังไม่ได้เลือก customer group ให้ disable sub group dropdown
|
||||
|
||||
---
|
||||
|
||||
## Scope 3: Seed Data
|
||||
|
||||
อัปเดต:
|
||||
|
||||
```txt
|
||||
src/db/seeds/foundation.seed.ts
|
||||
```
|
||||
|
||||
เพิ่ม seed category:
|
||||
|
||||
```txt
|
||||
crm_customer_group
|
||||
crm_customer_sub_group
|
||||
```
|
||||
|
||||
ตัวอย่าง:
|
||||
|
||||
```txt
|
||||
crm_customer_group:
|
||||
- industrial
|
||||
- construction
|
||||
- government
|
||||
- dealer
|
||||
- other
|
||||
|
||||
crm_customer_sub_group:
|
||||
- factory
|
||||
- warehouse
|
||||
- logistics
|
||||
- contractor
|
||||
- consultant
|
||||
- state_enterprise
|
||||
- government_agency
|
||||
- reseller
|
||||
- other
|
||||
```
|
||||
|
||||
Relationship ตัวอย่าง:
|
||||
|
||||
```txt
|
||||
industrial
|
||||
- factory
|
||||
- warehouse
|
||||
- logistics
|
||||
|
||||
construction
|
||||
- contractor
|
||||
- consultant
|
||||
|
||||
government
|
||||
- state_enterprise
|
||||
- government_agency
|
||||
|
||||
dealer
|
||||
- reseller
|
||||
|
||||
other
|
||||
- other
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* seed idempotent
|
||||
* no hardcoded organizationId
|
||||
* parent option must be created before child options
|
||||
* child options must set parentId
|
||||
* do not duplicate options if seed runs again
|
||||
|
||||
---
|
||||
|
||||
## Scope 4: Customer Schema / Zod
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/crm/customers/schemas/customer.schema.ts
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ts
|
||||
customerSubGroup: z.string().optional().nullable()
|
||||
```
|
||||
|
||||
หรือใช้ field name ที่มีอยู่จริง เช่น:
|
||||
|
||||
```ts
|
||||
custSubGroup: z.string().optional().nullable()
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* Create Customer ต้องรับ field นี้ได้
|
||||
* Edit Customer ต้อง load ค่าเดิมได้
|
||||
* ถ้าเปลี่ยน customerGroup แล้ว customerSubGroup ไม่อยู่ใต้ parent ใหม่ ให้ clear ค่า customerSubGroup
|
||||
|
||||
---
|
||||
|
||||
## Scope 5: API / Server Service
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/crm/customers/server/service.ts
|
||||
src/app/api/crm/customers/route.ts
|
||||
src/app/api/crm/customers/[id]/route.ts
|
||||
src/features/crm/customers/api/types.ts
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* create customer save customerSubGroup
|
||||
* update customer save customerSubGroup
|
||||
* list/detail return customerSubGroup
|
||||
* validate sub group parent matches selected group ถ้าทำได้ง่าย
|
||||
* if invalid parent-child relation, return user-safe error
|
||||
|
||||
---
|
||||
|
||||
## Scope 6: Customer Form UI
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/crm/customers/components/customer-form-sheet.tsx
|
||||
```
|
||||
|
||||
Add field:
|
||||
|
||||
```txt
|
||||
Customer Group
|
||||
Customer Sub Group
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
* Customer Group dropdown uses `crm_customer_group`
|
||||
* Customer Sub Group dropdown uses `crm_customer_sub_group`
|
||||
* Customer Sub Group filtered by selected group option `parentId`
|
||||
* If no Customer Group selected:
|
||||
|
||||
* disable Customer Sub Group
|
||||
* placeholder: "Select customer group first"
|
||||
* If Customer Group changes:
|
||||
|
||||
* reset Customer Sub Group
|
||||
* If edit mode:
|
||||
|
||||
* load existing Customer Group
|
||||
* load filtered Customer Sub Group
|
||||
* selected value must display correctly
|
||||
|
||||
---
|
||||
|
||||
## Scope 7: Customer List / Detail
|
||||
|
||||
Update if relevant:
|
||||
|
||||
```txt
|
||||
src/features/crm/customers/components/customer-columns.tsx
|
||||
src/features/crm/customers/components/customer-detail.tsx
|
||||
```
|
||||
|
||||
Display:
|
||||
|
||||
```txt
|
||||
Customer Group
|
||||
Customer Sub Group
|
||||
```
|
||||
|
||||
If label lookup is available, show label instead of raw code.
|
||||
|
||||
---
|
||||
|
||||
## Scope 8: Query Freshness
|
||||
|
||||
After create/update:
|
||||
|
||||
* invalidate customer list
|
||||
* invalidate customer detail
|
||||
* form closes after success
|
||||
* table/detail show new customer sub group immediately
|
||||
|
||||
Follow existing CRUD freshness rule.
|
||||
|
||||
---
|
||||
|
||||
## Scope 9: Regression Check
|
||||
|
||||
Verify:
|
||||
|
||||
* create customer with group + sub group
|
||||
* create customer without sub group
|
||||
* edit customer group changes and sub group resets
|
||||
* edit customer retains valid sub group
|
||||
* invalid sub group under another group is rejected or cleared
|
||||
* customer list/detail displays value
|
||||
|
||||
---
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
Do NOT implement:
|
||||
|
||||
```txt
|
||||
Customer group management UI
|
||||
Inline create master option
|
||||
Large RBAC rewrite
|
||||
Customer import/export
|
||||
Dashboard changes
|
||||
Report changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
After completion, summarize:
|
||||
|
||||
1. Files Added
|
||||
2. Files Modified
|
||||
3. Schema Field Used/Added
|
||||
4. Master Options Seeded
|
||||
5. Form Behavior Added
|
||||
6. Parent/Child Filtering Behavior
|
||||
7. Validation Added
|
||||
8. Verification Result
|
||||
9. Remaining Risks
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
* Add/Edit Customer has Customer Sub Group field
|
||||
* Customer Sub Group uses `crm_customer_sub_group`
|
||||
* Sub Group filters by selected Customer Group parent
|
||||
* Changing Customer Group resets invalid Sub Group
|
||||
* Create customer saves Sub Group
|
||||
* Edit customer saves Sub Group
|
||||
* Detail/list can show Sub Group
|
||||
* Mutation refreshes customer table/detail
|
||||
337
plans/task-h.2.1.md
Normal file
337
plans/task-h.2.1.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Task H.2.1: Reseed PDFME Template Mappings for Dynamic Topic Engine
|
||||
|
||||
## Goal
|
||||
|
||||
ปรับ seed data ของ PDFME template mappings ให้สอดคล้องกับ Task H.2
|
||||
|
||||
หลัง H.2 ระบบใช้ Dynamic Topic Engine แล้ว ดังนั้น `topic`, `data_topic`, และ dynamic topic input keys ไม่ควรถูกจัดการผ่าน `crm_document_template_mappings` แบบ static อีกต่อไป
|
||||
|
||||
---
|
||||
|
||||
# Background
|
||||
|
||||
Task H.2 เพิ่ม:
|
||||
|
||||
* PDFME transform utilities
|
||||
* Dynamic Topic Engine
|
||||
* Product topic mapping
|
||||
* `pdfme` section ใน quotation document data
|
||||
* runtime topic inputs
|
||||
* server-side schema cloning สำหรับ `topic` และ `data_topic`
|
||||
|
||||
ดังนั้น seed mappings ต้องถูก cleanup ให้ตรงกับ model ใหม่
|
||||
|
||||
---
|
||||
|
||||
# Must Read
|
||||
|
||||
```txt
|
||||
docs/implementation/task-h2-pdfme-mapping-transform-hotfix.md
|
||||
docs/implementation/pdfme-runtime-audit.md
|
||||
docs/implementation/pdfme-legacy-audit.md
|
||||
|
||||
src/db/seeds/foundation.seed.ts
|
||||
src/features/foundation/document-template/**
|
||||
src/features/crm/quotations/document/server/**
|
||||
src/pdfme_template/**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.1: Inspect Current Template Placeholders
|
||||
|
||||
Extract placeholders from current default templates:
|
||||
|
||||
```txt
|
||||
src/pdfme_template/ALLA_template_pdfme_fainal3.json
|
||||
src/pdfme_template/ONVALLA_template_pdfme_fainal3.json
|
||||
```
|
||||
|
||||
Expected static placeholders:
|
||||
|
||||
```txt
|
||||
customer_name
|
||||
customer_addr
|
||||
customer_tel
|
||||
customer_email
|
||||
customer_att
|
||||
project_name
|
||||
site_location
|
||||
quotation_date
|
||||
quotation_code
|
||||
quotation_price
|
||||
currency
|
||||
exclusion_data
|
||||
```
|
||||
|
||||
Dynamic topic template schemas:
|
||||
|
||||
```txt
|
||||
topic
|
||||
data_topic
|
||||
```
|
||||
|
||||
Signature placeholders, if present:
|
||||
|
||||
```txt
|
||||
app1
|
||||
app1_position
|
||||
app2
|
||||
app2_position
|
||||
app3
|
||||
app3_position
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.2: Static Mapping Seed
|
||||
|
||||
Update `foundation.seed.ts` so default pdfme mapping contains only static runtime placeholders:
|
||||
|
||||
```txt
|
||||
customer_name -> customer.name
|
||||
customer_addr -> customer.address
|
||||
customer_tel -> customer.phone
|
||||
customer_email -> customer.email
|
||||
customer_att -> quotation.attention
|
||||
project_name -> quotation.projectName
|
||||
site_location -> quotation.projectLocation
|
||||
quotation_date -> pdfme.quotation_date
|
||||
quotation_code -> quotation.code
|
||||
quotation_price -> pdfme.quotation_price
|
||||
currency -> quotation.currency
|
||||
exclusion_data -> pdfme.exclusion_data
|
||||
```
|
||||
|
||||
Recommended metadata:
|
||||
|
||||
```txt
|
||||
quotation_date:
|
||||
dataType = scalar
|
||||
formatMask = null
|
||||
sourcePath = pdfme.quotation_date
|
||||
|
||||
quotation_price:
|
||||
dataType = scalar
|
||||
formatMask = null
|
||||
sourcePath = pdfme.quotation_price
|
||||
|
||||
exclusion_data:
|
||||
dataType = table
|
||||
sourcePath = pdfme.exclusion_data
|
||||
```
|
||||
|
||||
Reason:
|
||||
|
||||
* date/currency are already formatted in `pdfme` section from H.2
|
||||
* do not format twice in mapping resolver
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.3: Retire Invalid Static Topic Mappings
|
||||
|
||||
Ensure seed removes or deactivates mappings for:
|
||||
|
||||
```txt
|
||||
topic
|
||||
data_topic
|
||||
item_topic
|
||||
data_topic_*
|
||||
topic_*
|
||||
item_topic_*
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* `topic` and `data_topic` are schema templates, not data placeholders
|
||||
* `item_topic` is legacy content placeholder inside `data_topic`, not a DB mapping
|
||||
* dynamic keys such as `topic_1_0` and `item_topic_1_0` are generated at runtime
|
||||
* do not store generated dynamic keys in DB
|
||||
|
||||
Implementation guidance:
|
||||
|
||||
If mappings exist from old seed:
|
||||
|
||||
```txt
|
||||
soft delete / delete / skip
|
||||
```
|
||||
|
||||
according to current template mapping service convention.
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.4: Signature Mapping Decision
|
||||
|
||||
If current template contains signature placeholders:
|
||||
|
||||
```txt
|
||||
app1
|
||||
app1_position
|
||||
app2
|
||||
app2_position
|
||||
app3
|
||||
app3_position
|
||||
```
|
||||
|
||||
For this task:
|
||||
|
||||
Option A recommended:
|
||||
|
||||
Seed them with safe placeholder values:
|
||||
|
||||
```txt
|
||||
app1 -> signatures.preparedBy.name
|
||||
app1_position -> signatures.preparedBy.position
|
||||
app2 -> signatures.approvedBy.name
|
||||
app2_position -> signatures.approvedBy.position
|
||||
app3 -> signatures.authorizedBy.name
|
||||
app3_position -> signatures.authorizedBy.position
|
||||
```
|
||||
|
||||
But only if `documentData.signatures` exists.
|
||||
|
||||
If not available yet:
|
||||
|
||||
* do not break PDF
|
||||
* use defaultValue = "-"
|
||||
* document as pending Task H.3
|
||||
|
||||
Rules:
|
||||
|
||||
* no approval signature business logic in this task
|
||||
* no new approval mapping strategy in this task
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.5: Seed Idempotency
|
||||
|
||||
Seed must be idempotent:
|
||||
|
||||
* run repeatedly without duplicate mappings
|
||||
* update existing wrong sourcePath
|
||||
* update existing wrong dataType
|
||||
* remove/retire invalid mappings
|
||||
* no hardcoded organizationId
|
||||
* apply for every organization
|
||||
* apply for both ALLA and ONVALLA templates if seeded
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.6: Runtime Verification
|
||||
|
||||
Extend or run audit script:
|
||||
|
||||
```txt
|
||||
scripts/audit-pdfme-runtime.ts
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
Static template placeholders:
|
||||
|
||||
```txt
|
||||
customer_name mappingExists = true
|
||||
customer_addr mappingExists = true
|
||||
customer_tel mappingExists = true
|
||||
customer_email mappingExists = true
|
||||
customer_att mappingExists = true
|
||||
project_name mappingExists = true
|
||||
site_location mappingExists = true
|
||||
quotation_date mappingExists = true
|
||||
quotation_code mappingExists = true
|
||||
quotation_price mappingExists = true
|
||||
currency mappingExists = true
|
||||
exclusion_data mappingExists = true
|
||||
```
|
||||
|
||||
Dynamic schemas:
|
||||
|
||||
```txt
|
||||
topic mappingExists = false, dynamicTemplate = true
|
||||
data_topic mappingExists = false, dynamicTemplate = true
|
||||
item_topic mappingExists = false, legacyContentToken = true
|
||||
```
|
||||
|
||||
PDFME input should contain runtime keys:
|
||||
|
||||
```txt
|
||||
topic_*
|
||||
item_topic_*
|
||||
```
|
||||
|
||||
but these must come from Dynamic Topic Engine, not DB mappings.
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1.7: Documentation
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
docs/implementation/task-h2-pdfme-mapping-transform-hotfix.md
|
||||
```
|
||||
|
||||
Add section:
|
||||
|
||||
```txt
|
||||
Seed Mapping Strategy After Dynamic Topic Engine
|
||||
```
|
||||
|
||||
Also update:
|
||||
|
||||
```txt
|
||||
docs/implementation/technical-debt.md
|
||||
```
|
||||
|
||||
Add note:
|
||||
|
||||
```txt
|
||||
Signature placeholders app1/app2/app3 still need Task H.3 strategy if not fully mapped.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Explicit Non-Scope
|
||||
|
||||
Do NOT implement:
|
||||
|
||||
```txt
|
||||
Approval signature strategy
|
||||
Visual template designer
|
||||
Dynamic topic editor
|
||||
Changing PDF layout
|
||||
Pixel-perfect comparison
|
||||
Report Center
|
||||
Dashboard changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Output
|
||||
|
||||
After completion, summarize:
|
||||
|
||||
1. Files Modified
|
||||
2. Static Mappings Seeded
|
||||
3. Invalid Topic Mappings Retired
|
||||
4. Signature Placeholder Handling
|
||||
5. Runtime Audit Result
|
||||
6. Remaining Risks
|
||||
7. Task H.3 Readiness
|
||||
|
||||
---
|
||||
|
||||
# Definition of Done
|
||||
|
||||
Task H.2.1 passes when:
|
||||
|
||||
* static PDFME mappings are complete
|
||||
* `exclusion_data` maps to `pdfme.exclusion_data`
|
||||
* `quotation_price` maps to `pdfme.quotation_price`
|
||||
* `quotation_date` maps to `pdfme.quotation_date`
|
||||
* `topic` is not mapped as static DB mapping
|
||||
* `data_topic` is not mapped as static DB mapping
|
||||
* `item_topic` is not mapped as static DB mapping
|
||||
* dynamic topic keys are generated at runtime only
|
||||
* seed is idempotent
|
||||
* runtime audit recognizes topic/data_topic as dynamic templates
|
||||
621
plans/task-h.2.md
Normal file
621
plans/task-h.2.md
Normal file
@@ -0,0 +1,621 @@
|
||||
# Task H.2: PDFME Dynamic Topic Engine + Mapping Hotfix
|
||||
|
||||
## Background
|
||||
|
||||
Current issue:
|
||||
|
||||
```txt
|
||||
PDFME quotation แสดงข้อมูลไม่ครบ
|
||||
Mapping field ไม่ถูกต้อง
|
||||
Topic จาก crm_quotation_topics / crm_quotation_topic_items ไม่แสดงครบ
|
||||
```
|
||||
|
||||
จาก Legacy Audit และ Runtime Audit พบว่า:
|
||||
|
||||
1. Current template มี placeholder บางตัวแต่ mapping ไม่ครบ
|
||||
2. `quotation_price` ยังส่งเป็น raw number
|
||||
3. `quotation_date` ยังเสี่ยงเป็น raw date
|
||||
4. `exclusion_data` ต้องเป็น `string[][]`
|
||||
5. `topic` และ `data_topic` ไม่ใช่ mapping ปกติ
|
||||
6. `topic` และ `data_topic` เป็น schema template สำหรับ dynamic topic rendering
|
||||
|
||||
---
|
||||
|
||||
# Must Read
|
||||
|
||||
```txt
|
||||
docs/implementation/pdfme-legacy-audit.md
|
||||
docs/implementation/pdfme-runtime-audit.md
|
||||
docs/implementation/technical-debt.md
|
||||
|
||||
src/features/foundation/pdf-generator/**
|
||||
src/features/foundation/document-template/**
|
||||
src/features/crm/quotations/document/**
|
||||
src/features/crm/quotations/**
|
||||
src/db/schema.ts
|
||||
src/db/seeds/foundation.seed.ts
|
||||
src/pdfme_template/**
|
||||
```
|
||||
|
||||
Legacy reference files:
|
||||
|
||||
```txt
|
||||
pdf-topic.type.ts
|
||||
pdf-topic-input.ts
|
||||
layout-engine.ts
|
||||
build-topic-schema.ts
|
||||
pdf-topic-engine.ts
|
||||
quotation-preview.tsx
|
||||
```
|
||||
|
||||
Important legacy behavior:
|
||||
|
||||
* `topic` displays `crm_quotation_topics.topicType`
|
||||
* `data_topic` displays rows from `crm_quotation_topic_items.content`
|
||||
* `topic` and `data_topic` are cloned dynamically
|
||||
* Runtime schema names become:
|
||||
|
||||
* `topic_0_0`
|
||||
* `item_topic_0_0`
|
||||
* `topic_0_1`
|
||||
* `item_topic_0_1`
|
||||
|
||||
---
|
||||
|
||||
# Goal
|
||||
|
||||
Port the legacy PDFME topic rendering behavior into ALLA OS CRM vNext server-side PDF pipeline.
|
||||
|
||||
Fix current mapping issues without introducing a visual template designer.
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.1: PDFME Transform Utilities
|
||||
|
||||
Create:
|
||||
|
||||
```txt
|
||||
src/features/crm/quotations/document/server/pdfme-transforms.ts
|
||||
```
|
||||
|
||||
Implement:
|
||||
|
||||
```ts
|
||||
formatPdfDate(dateString: string | Date | null | undefined, language?: "th" | "en"): string
|
||||
|
||||
formatPdfCurrency(
|
||||
amount: number | string | null | undefined,
|
||||
currencyCode?: string
|
||||
): string
|
||||
|
||||
formatTopicItems(
|
||||
topic?: { items?: Array<{ content?: string | null; sortOrder?: number | null }> }
|
||||
): string[][]
|
||||
|
||||
normalizePdfmeTable(
|
||||
value: unknown,
|
||||
columns?: Array<{ sourceField: string; formatMask?: string | null }>
|
||||
): string[][]
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* empty scalar fallback = `-`
|
||||
* empty topic fallback = `[["-"]]`
|
||||
* all pdfme table inputs must be `string[][]`
|
||||
* currency supports at least:
|
||||
|
||||
* THB
|
||||
* USD
|
||||
* EUR
|
||||
* `date_th` uses Buddhist calendar
|
||||
* `date_en` uses Gregorian calendar
|
||||
* never return `undefined` to pdfme
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.2: Dynamic Topic Types
|
||||
|
||||
Create:
|
||||
|
||||
```txt
|
||||
src/features/crm/quotations/document/server/pdf-topic.type.ts
|
||||
```
|
||||
|
||||
Types:
|
||||
|
||||
```ts
|
||||
export type PdfTopicItem = {
|
||||
id: string | number;
|
||||
content: string;
|
||||
sortOrder?: number | null;
|
||||
};
|
||||
|
||||
export type PdfTopic = {
|
||||
id?: string | number;
|
||||
topicType: string;
|
||||
sortOrder?: number | null;
|
||||
items?: PdfTopicItem[];
|
||||
};
|
||||
|
||||
export type PdfTopicEngineOptions = {
|
||||
targetPageIndex?: number;
|
||||
pageStartY?: number;
|
||||
pageBottomY?: number;
|
||||
topicSpacing?: number;
|
||||
keepTogetherNames?: string[];
|
||||
topicTemplateName?: string;
|
||||
topicDataTemplateName?: string;
|
||||
rowHeight?: number;
|
||||
keepTogetherMinSpace?: number;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.3: PDFME Topic Engine
|
||||
|
||||
Create:
|
||||
|
||||
```txt
|
||||
src/features/crm/quotations/document/server/pdf-topic-engine.ts
|
||||
```
|
||||
|
||||
Port behavior from legacy:
|
||||
|
||||
```txt
|
||||
buildTopicSchemas()
|
||||
```
|
||||
|
||||
Function:
|
||||
|
||||
```ts
|
||||
buildPdfTopicTemplate(
|
||||
template: Template,
|
||||
topics: PdfTopic[],
|
||||
options?: PdfTopicEngineOptions
|
||||
): {
|
||||
template: Template;
|
||||
topicInputs: Record<string, string[][]>;
|
||||
}
|
||||
```
|
||||
|
||||
Required behavior:
|
||||
|
||||
1. Read target page from `template.schemas[targetPageIndex]`
|
||||
2. Locate schema named `topic`
|
||||
3. Locate schema named `data_topic`
|
||||
4. Treat both as template schemas
|
||||
5. Remove original `topic` and `data_topic` from static schemas
|
||||
6. Sort topics by `sortOrder`
|
||||
7. Clone `topic` for each topic
|
||||
8. Clone `data_topic` for each topic
|
||||
9. Rename clones to stable dynamic keys:
|
||||
|
||||
```txt
|
||||
topic_<pageIndex>_<topicIndex>
|
||||
item_topic_<pageIndex>_<topicIndex>
|
||||
```
|
||||
|
||||
10. Set topic label input:
|
||||
|
||||
```ts
|
||||
topic_<pageIndex>_<topicIndex> = [[topic.topicType]]
|
||||
```
|
||||
|
||||
11. Set topic items input:
|
||||
|
||||
```ts
|
||||
item_topic_<pageIndex>_<topicIndex> = [
|
||||
[item.content],
|
||||
[item.content]
|
||||
]
|
||||
```
|
||||
|
||||
12. Calculate table height from row count
|
||||
13. Paginate when topic block exceeds `pageBottomY`
|
||||
14. Move signature/closing group together
|
||||
15. Rebuild `template.schemas` with generated pages
|
||||
16. Return final template + topicInputs
|
||||
|
||||
Default options:
|
||||
|
||||
```ts
|
||||
{
|
||||
targetPageIndex: 1,
|
||||
pageStartY: 35,
|
||||
pageBottomY: 250,
|
||||
topicSpacing: 10,
|
||||
keepTogetherNames: [
|
||||
"Please_do_not",
|
||||
"yours_faithfuly",
|
||||
"app1",
|
||||
"app1_position",
|
||||
"app2",
|
||||
"app2_position",
|
||||
"app3",
|
||||
"app3_position"
|
||||
],
|
||||
topicTemplateName: "topic",
|
||||
topicDataTemplateName: "data_topic",
|
||||
rowHeight: 7,
|
||||
keepTogetherMinSpace: 60
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* if template has no `topic` or `data_topic`, do not crash PDF generation
|
||||
* return original template and empty topicInputs with warning
|
||||
* topic items empty => `[["-"]]`
|
||||
* keep together block must not overlap topic data
|
||||
* no client-only PDFME UI code
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.4: Product Type Topic Mapping
|
||||
|
||||
Create:
|
||||
|
||||
```txt
|
||||
src/features/crm/quotations/document/server/topic-mapping.ts
|
||||
```
|
||||
|
||||
Implement:
|
||||
|
||||
```ts
|
||||
export const QUOTATION_TOPIC_TYPE_MAPPING = {
|
||||
crane: {
|
||||
scope: "scope_of_work",
|
||||
exclusion: "exclusion_from_scope_of_supply",
|
||||
payment: "terms_of_payment_upon_progress_of_each_item",
|
||||
delivery: "delivery_date",
|
||||
warranty: "warranty"
|
||||
},
|
||||
dockdoor: {
|
||||
scope: "dock_scope_of_work",
|
||||
exclusion: "exclusion_of_supply_installation",
|
||||
payment: "payment_conditions",
|
||||
delivery: "delivery_date",
|
||||
warranty: "warranty"
|
||||
},
|
||||
solarcell: {
|
||||
scope: "solarcell_scope_of_work",
|
||||
exclusion: "solarcell_exclusion_of_supply_installation",
|
||||
payment: "solarcell_payment_conditions",
|
||||
delivery: "solarcell_delivery",
|
||||
warranty: "solarcell_warranty"
|
||||
},
|
||||
default: {
|
||||
scope: "scope_of_work",
|
||||
exclusion: "exclusion_from_scope_of_supply",
|
||||
payment: "payment_conditions",
|
||||
delivery: "delivery",
|
||||
warranty: "warranty"
|
||||
}
|
||||
} as const;
|
||||
```
|
||||
|
||||
Add helper:
|
||||
|
||||
```ts
|
||||
getQuotationTopicMapping(quotationType?: string | null)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* use `quotation.quotationType` or current schema equivalent
|
||||
* fallback to `default`
|
||||
* do not hardcode crane only
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.5: Document Data Builder Update
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/crm/quotations/document/server/service.ts
|
||||
```
|
||||
|
||||
`buildQuotationDocumentData()` must return:
|
||||
|
||||
```ts
|
||||
{
|
||||
company,
|
||||
branch,
|
||||
customer,
|
||||
contact,
|
||||
quotation,
|
||||
items,
|
||||
quotationCustomers,
|
||||
topics,
|
||||
approval,
|
||||
signatures,
|
||||
pdfme
|
||||
}
|
||||
```
|
||||
|
||||
Add `pdfme` section:
|
||||
|
||||
```ts
|
||||
pdfme: {
|
||||
quotation_date: string;
|
||||
quotation_price: string;
|
||||
exclusion_data: string[][];
|
||||
topic_inputs?: Record<string, string[][]>;
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
* keep normalized document data for app preview
|
||||
* add pdfme-ready compatibility fields
|
||||
* do not remove existing fields
|
||||
* topics must include data from:
|
||||
|
||||
* crm_quotation_topics
|
||||
* crm_quotation_topic_items
|
||||
* topics must be sorted by `sortOrder`
|
||||
* topic items must be sorted by `sortOrder`
|
||||
* `exclusion_data` must use product type mapping
|
||||
* if no exclusion topic exists, return `[["-"]]`
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.6: Template Input Mapping Update
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/foundation/document-template/server/service.ts
|
||||
```
|
||||
|
||||
Enhance:
|
||||
|
||||
```ts
|
||||
mapDocumentDataToTemplateInput()
|
||||
```
|
||||
|
||||
Must support:
|
||||
|
||||
1. `formatMask = currency_THB`
|
||||
2. `formatMask = currency_USD`
|
||||
3. `formatMask = currency_EUR`
|
||||
4. `formatMask = date_th`
|
||||
5. `formatMask = date_en`
|
||||
6. table values already shaped as `string[][]`
|
||||
7. object array to `string[][]` conversion only when table columns exist
|
||||
8. fallback value when sourcePath resolves `null` or `undefined`
|
||||
|
||||
Important:
|
||||
|
||||
If source path points to:
|
||||
|
||||
```txt
|
||||
pdfme.exclusion_data
|
||||
```
|
||||
|
||||
or any `string[][]`
|
||||
|
||||
preserve table shape.
|
||||
|
||||
Do not convert to string.
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.7: PDF Generator Integration
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/features/foundation/pdf-generator/server/service.ts
|
||||
```
|
||||
|
||||
Before calling pdfme `generate()`:
|
||||
|
||||
1. Build documentData
|
||||
2. Resolve template
|
||||
3. Resolve mappings
|
||||
4. Build base templateInput
|
||||
5. Run dynamic topic engine:
|
||||
|
||||
```ts
|
||||
const { template: topicTemplate, topicInputs } =
|
||||
buildPdfTopicTemplate(template, documentData.topics);
|
||||
```
|
||||
|
||||
6. Merge inputs:
|
||||
|
||||
```ts
|
||||
const inputs = [
|
||||
{
|
||||
...templateInput,
|
||||
...topicInputs
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
7. Generate PDF with final template
|
||||
|
||||
Rules:
|
||||
|
||||
* dynamic topic engine must run for both preview and approved PDF
|
||||
* if template has no topic/data_topic schemas, continue with base template
|
||||
* server-only
|
||||
* no @pdfme/ui usage
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.8: Template Seed Mapping Fix
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
src/db/seeds/foundation.seed.ts
|
||||
```
|
||||
|
||||
Ensure default pdfme mappings include:
|
||||
|
||||
```txt
|
||||
customer_name -> customer.name
|
||||
customer_addr -> customer.address
|
||||
customer_tel -> customer.phone
|
||||
customer_email -> customer.email
|
||||
customer_att -> quotation.attention
|
||||
project_name -> quotation.projectName
|
||||
site_location -> quotation.projectLocation
|
||||
quotation_date -> pdfme.quotation_date
|
||||
quotation_code -> quotation.code
|
||||
quotation_price -> pdfme.quotation_price
|
||||
currency -> quotation.currency
|
||||
exclusion_data -> pdfme.exclusion_data
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
Do NOT add `topic` and `data_topic` as normal mappings.
|
||||
|
||||
They are dynamic schema templates.
|
||||
|
||||
Rules:
|
||||
|
||||
* idempotent
|
||||
* update existing sourcePath if wrong
|
||||
* add missing mapping
|
||||
* no hardcoded organizationId
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.9: Current Template Compatibility
|
||||
|
||||
Current template contains:
|
||||
|
||||
* page 1: `exclusion_data`
|
||||
* page 2: `topic`
|
||||
* page 2: `data_topic`
|
||||
* page 2: signature/closing block
|
||||
|
||||
This task must support exactly that.
|
||||
|
||||
Rules:
|
||||
|
||||
* `exclusion_data` is static mapped table on page 1
|
||||
* `topic` / `data_topic` are dynamic templates on page 2
|
||||
* original `topic` / `data_topic` should not remain as placeholder rows if dynamic topics exist
|
||||
* signature block should remain after dynamic topics
|
||||
* long topics should add pages if needed
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.10: Verification
|
||||
|
||||
Use or extend existing scripts:
|
||||
|
||||
```txt
|
||||
npm run seed:task-h1-fixture
|
||||
npm run verify:task-h1
|
||||
```
|
||||
|
||||
Add verification for:
|
||||
|
||||
```txt
|
||||
quotation_price formatted
|
||||
quotation_date formatted
|
||||
exclusion_data table is non-empty or "-"
|
||||
topic dynamic keys exist
|
||||
topic inputs exist
|
||||
PDF generation succeeds
|
||||
PDF does not contain unresolved {exclusion_data}
|
||||
PDF does not contain unresolved {quotation_price}
|
||||
PDF does not contain unresolved {item_topic}
|
||||
```
|
||||
|
||||
Optional debug output:
|
||||
|
||||
```txt
|
||||
docs/implementation/pdfme-h2-verification.md
|
||||
```
|
||||
|
||||
Include:
|
||||
|
||||
```txt
|
||||
template placeholder list
|
||||
generated dynamic topic keys
|
||||
templateInput sample
|
||||
topicInputs sample
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Scope H2.11: Documentation
|
||||
|
||||
Update:
|
||||
|
||||
```txt
|
||||
docs/implementation/technical-debt.md
|
||||
```
|
||||
|
||||
Add or confirm:
|
||||
|
||||
```txt
|
||||
Payment/warranty/delivery are rendered through dynamic topic section, not fixed static placeholders
|
||||
Dynamic extra topics now supported through topic/data_topic engine
|
||||
Approval signature fields still require dedicated mapping strategy if real approver names/positions are needed
|
||||
Pixel-perfect legacy comparison not implemented
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Explicit Non-Scope
|
||||
|
||||
Do NOT implement:
|
||||
|
||||
```txt
|
||||
Visual template designer
|
||||
@pdfme/ui editor
|
||||
Client-side PDF generation
|
||||
Full pixel-perfect visual regression
|
||||
Approval signature real mapping
|
||||
Changing template design manually unless required
|
||||
Replacing storage/artifact lifecycle
|
||||
Report Center
|
||||
Dashboard changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Output
|
||||
|
||||
After completion, summarize:
|
||||
|
||||
1. Files Added
|
||||
2. Files Modified
|
||||
3. Transform Utilities Added
|
||||
4. Dynamic Topic Engine Added
|
||||
5. Product Topic Mapping Added
|
||||
6. Template Mapping Fixed
|
||||
7. PDF Generator Integration
|
||||
8. Verification Result
|
||||
9. Remaining PDFME Gaps
|
||||
10. Task K Readiness
|
||||
|
||||
---
|
||||
|
||||
# Definition of Done
|
||||
|
||||
Task H.2 passes when:
|
||||
|
||||
* `quotation_price` renders as formatted currency
|
||||
* `quotation_date` renders as formatted date
|
||||
* `exclusion_data` maps and renders as `string[][]`
|
||||
* `topic` and `data_topic` are treated as dynamic templates
|
||||
* `crm_quotation_topics.topicType` renders as dynamic topic label
|
||||
* `crm_quotation_topic_items.content` renders as dynamic topic rows
|
||||
* multiple topics render correctly
|
||||
* long topic list paginates
|
||||
* signature/closing block does not overlap topics
|
||||
* PDF generation succeeds for preview PDF
|
||||
* PDF generation succeeds for approved PDF
|
||||
* unresolved placeholders do not appear in generated PDF
|
||||
* no template designer added
|
||||
Reference in New Issue
Block a user