Files
alla-allaos-fullstack/plans/task-h.2.md
2026-06-18 08:58:01 +07:00

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 พบว่า:

  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

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:

  • 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:

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_th uses Buddhist calendar

  • date_en uses Gregorian calendar

  • never return undefined to 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:

  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:
topic_<pageIndex>_<topicIndex>
item_topic_<pageIndex>_<topicIndex>
  1. Set topic label input:
topic_<pageIndex>_<topicIndex> = [[topic.topicType]]
  1. Set topic items input:
item_topic_<pageIndex>_<topicIndex> = [
  [item.content],
  [item.content]
]
  1. Calculate table height from row count
  2. Paginate when topic block exceeds pageBottomY
  3. Move signature/closing group together
  4. Rebuild template.schemas with generated pages
  5. 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 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:

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.quotationType or 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_data must 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:

  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:

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():

  1. Build documentData
  2. Resolve template
  3. Resolve mappings
  4. Build base templateInput
  5. Run dynamic topic engine:
const { template: topicTemplate, topicInputs } =
  buildPdfTopicTemplate(template, documentData.topics);
  1. Merge inputs:
const inputs = [
  {
    ...templateInput,
    ...topicInputs
  }
]
  1. 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_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:

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:

  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