hotfix-add subgroup customer
This commit is contained in:
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