622 lines
12 KiB
Markdown
622 lines
12 KiB
Markdown
# 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
|