# 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; } ``` 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__ item_topic__ ``` 10. Set topic label input: ```ts topic__ = [[topic.topicType]] ``` 11. Set topic items input: ```ts item_topic__ = [ [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; } ``` 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