12 KiB
Task H.2: PDFME Dynamic Topic Engine + Mapping Hotfix
Background
Current issue:
PDFME quotation แสดงข้อมูลไม่ครบ
Mapping field ไม่ถูกต้อง
Topic จาก crm_quotation_topics / crm_quotation_topic_items ไม่แสดงครบ
จาก Legacy Audit และ Runtime Audit พบว่า:
- Current template มี placeholder บางตัวแต่ mapping ไม่ครบ
quotation_priceยังส่งเป็น raw numberquotation_dateยังเสี่ยงเป็น raw dateexclusion_dataต้องเป็นstring[][]topicและdata_topicไม่ใช่ mapping ปกติtopicและdata_topicเป็น schema template สำหรับ dynamic topic rendering
Must Read
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:
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:
-
topicdisplayscrm_quotation_topics.topicType -
data_topicdisplays rows fromcrm_quotation_topic_items.content -
topicanddata_topicare cloned dynamically -
Runtime schema names become:
topic_0_0item_topic_0_0topic_0_1item_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:
src/features/crm/quotations/document/server/pdfme-transforms.ts
Implement:
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_thuses Buddhist calendar -
date_enuses Gregorian calendar -
never return
undefinedto pdfme
Scope H2.2: Dynamic Topic Types
Create:
src/features/crm/quotations/document/server/pdf-topic.type.ts
Types:
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:
src/features/crm/quotations/document/server/pdf-topic-engine.ts
Port behavior from legacy:
buildTopicSchemas()
Function:
buildPdfTopicTemplate(
template: Template,
topics: PdfTopic[],
options?: PdfTopicEngineOptions
): {
template: Template;
topicInputs: Record<string, string[][]>;
}
Required behavior:
- Read target page from
template.schemas[targetPageIndex] - Locate schema named
topic - Locate schema named
data_topic - Treat both as template schemas
- Remove original
topicanddata_topicfrom static schemas - Sort topics by
sortOrder - Clone
topicfor each topic - Clone
data_topicfor each topic - Rename clones to stable dynamic keys:
topic_<pageIndex>_<topicIndex>
item_topic_<pageIndex>_<topicIndex>
- Set topic label input:
topic_<pageIndex>_<topicIndex> = [[topic.topicType]]
- Set topic items input:
item_topic_<pageIndex>_<topicIndex> = [
[item.content],
[item.content]
]
- Calculate table height from row count
- Paginate when topic block exceeds
pageBottomY - Move signature/closing group together
- Rebuild
template.schemaswith generated pages - Return final template + topicInputs
Default options:
{
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
topicordata_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:
src/features/crm/quotations/document/server/topic-mapping.ts
Implement:
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:
getQuotationTopicMapping(quotationType?: string | null)
Rules:
- use
quotation.quotationTypeor current schema equivalent - fallback to
default - do not hardcode crane only
Scope H2.5: Document Data Builder Update
Update:
src/features/crm/quotations/document/server/service.ts
buildQuotationDocumentData() must return:
{
company,
branch,
customer,
contact,
quotation,
items,
quotationCustomers,
topics,
approval,
signatures,
pdfme
}
Add pdfme section:
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_datamust use product type mapping -
if no exclusion topic exists, return
[["-"]]
Scope H2.6: Template Input Mapping Update
Update:
src/features/foundation/document-template/server/service.ts
Enhance:
mapDocumentDataToTemplateInput()
Must support:
formatMask = currency_THBformatMask = currency_USDformatMask = currency_EURformatMask = date_thformatMask = date_en- table values already shaped as
string[][] - object array to
string[][]conversion only when table columns exist - fallback value when sourcePath resolves
nullorundefined
Important:
If source path points to:
pdfme.exclusion_data
or any string[][]
preserve table shape.
Do not convert to string.
Scope H2.7: PDF Generator Integration
Update:
src/features/foundation/pdf-generator/server/service.ts
Before calling pdfme generate():
- Build documentData
- Resolve template
- Resolve mappings
- Build base templateInput
- Run dynamic topic engine:
const { template: topicTemplate, topicInputs } =
buildPdfTopicTemplate(template, documentData.topics);
- Merge inputs:
const inputs = [
{
...templateInput,
...topicInputs
}
]
- 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:
src/db/seeds/foundation.seed.ts
Ensure default pdfme mappings include:
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_datais static mapped table on page 1topic/data_topicare dynamic templates on page 2- original
topic/data_topicshould 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:
npm run seed:task-h1-fixture
npm run verify:task-h1
Add verification for:
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:
docs/implementation/pdfme-h2-verification.md
Include:
template placeholder list
generated dynamic topic keys
templateInput sample
topicInputs sample
Scope H2.11: Documentation
Update:
docs/implementation/technical-debt.md
Add or confirm:
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:
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:
- Files Added
- Files Modified
- Transform Utilities Added
- Dynamic Topic Engine Added
- Product Topic Mapping Added
- Template Mapping Fixed
- PDF Generator Integration
- Verification Result
- Remaining PDFME Gaps
- Task K Readiness
Definition of Done
Task H.2 passes when:
quotation_pricerenders as formatted currencyquotation_daterenders as formatted dateexclusion_datamaps and renders asstring[][]topicanddata_topicare treated as dynamic templatescrm_quotation_topics.topicTyperenders as dynamic topic labelcrm_quotation_topic_items.contentrenders 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