# 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
