hotfix-add subgroup customer
This commit is contained in:
910
docs/implementation/pdfme-runtime-audit.md
Normal file
910
docs/implementation/pdfme-runtime-audit.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# PDFME Runtime Audit Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This audit compares the current PDFME template and mapping system against the legacy implementation and identifies runtime issues that cause incomplete or incorrectly mapped data in generated quotation PDFs.
|
||||
|
||||
**Key Finding**: The current implementation has significant gaps in placeholder mappings, missing topic sections, and incorrect data formats that prevent PDFME from displaying complete quotation information.
|
||||
|
||||
**Total Template Placeholders**: 20
|
||||
**Total Required Mappings**: 20
|
||||
**Missing Mappings**: 8
|
||||
**Critical Issues**: 5
|
||||
|
||||
---
|
||||
|
||||
## 1. Template Analysis
|
||||
|
||||
### 1.1 Current Template File
|
||||
**Path**: `src/pdfme_template/ALLA_template_pdfme_fainal3.json`
|
||||
|
||||
### 1.2 Placeholder Extraction
|
||||
|
||||
The following placeholders were extracted from the current template schema:
|
||||
|
||||
| Placeholder Key | Field Type | Content Pattern | Position (Page 1) |
|
||||
|-----------------|------------|-----------------|-------------------|
|
||||
| `customer_name` | text | `{customer_name}` | Header |
|
||||
| `customer_addr` | text | `{customer_addr}` | Header |
|
||||
| `customer_tel` | text | `{customer_tel}` | Header |
|
||||
| `customer_email` | text | `{customer_email}` | Header |
|
||||
| `customer_att` | text | `{customer_att}` | Header |
|
||||
| `project_name` | text | `{project_name}` | Header |
|
||||
| `site_location` | text | `{site_location}` | Header |
|
||||
| `quotation_date` | text | `{quotation_date}` | Header |
|
||||
| `quotation_code` | text | `{quotation_code}` | Header |
|
||||
| `quotation_price` | table (cell) | `{quotation_price}` | Price section |
|
||||
| `currency` | table (cell) | `{currency}` | Price section |
|
||||
| `exclusion_data` | table | `[[\"{exclusion_data}\"]]` | Exclusion section |
|
||||
|
||||
**Static Content (No Mapping Required)**:
|
||||
- Company header (company1, company2, company_addr1, company_addr2, company_tel, company_email, company_tax)
|
||||
- Labels (Date, Quotation No, Tel, Email, etc.)
|
||||
- Colons and separators
|
||||
- "Dear sirs" text
|
||||
|
||||
---
|
||||
|
||||
## 2. Runtime Audit Results
|
||||
|
||||
### 2.1 Placeholder Mapping Status
|
||||
|
||||
| Placeholder | Template Exists | Mapping Exists | Source Path | Expected Type | Actual Type | Status |
|
||||
|-------------|-----------------|----------------|-------------|---------------|-------------|--------|
|
||||
| `customer_name` | ✅ | ✅ | `customer.name` | string | string | ✅ OK |
|
||||
| `customer_addr` | ✅ | ✅ | `customer.address` | string | string | ✅ OK |
|
||||
| `customer_tel` | ✅ | ✅ | `customer.contacts[primary].phone` | string | string | ✅ OK |
|
||||
| `customer_email` | ✅ | ✅ | `customer.contacts[primary].email` | string | string | ✅ OK |
|
||||
| `customer_att` | ✅ | ✅ | `quotation.attention` | string | string | ✅ OK |
|
||||
| `project_name` | ✅ | ✅ | `quotation.projectName` | string | string | ✅ OK |
|
||||
| `site_location` | ✅ | ✅ | `quotation.projectLocation` | string | string | ✅ OK |
|
||||
| `quotation_date` | ✅ | ✅ | `quotation.quotationDate` | string | string | ⚠️ **Check Format** |
|
||||
| `quotation_code` | ✅ | ✅ | `quotation.code` | string | string | ✅ OK |
|
||||
| `quotation_price` | ✅ | ✅ | `quotation.totalAmount` | string (formatted) | number | ⚠️ **Type Mismatch** |
|
||||
| `currency` | ✅ | ✅ | `quotation.currency` | string | string | ✅ OK |
|
||||
| `exclusion_data` | ✅ | ❌ | - | `string[][]` | undefined | ❌ **MISSING** |
|
||||
|
||||
### 2.2 Legacy Placeholders Not in Current Template
|
||||
|
||||
The following placeholders existed in the legacy implementation but are **not present** in the current template:
|
||||
|
||||
| Placeholder | Legacy Purpose | Status |
|
||||
|-------------|----------------|--------|
|
||||
| `exclusion_label` | Topic label (static) | ❌ **Not in template** |
|
||||
| `Terms_of_payment` | Payment topic label | ❌ **Not in template** |
|
||||
| `data_term_of_payment` | Payment topic data | ❌ **Not in template** |
|
||||
| `warranty` | Warranty topic label | ❌ **Not in template** |
|
||||
| `data_warranty` | Warranty topic data | ❌ **Not in template** |
|
||||
| `Delivery` | Delivery topic label | ❌ **Not in template** |
|
||||
| `data_delivery` | Delivery topic data | ❌ **Not in template** |
|
||||
| `Affter service` | After-service topic label | ❌ **Not in template** |
|
||||
| `data_affter_service` | After-service topic data | ❌ **Not in template** |
|
||||
| `topic` | General topic label | ❌ **Not in template** |
|
||||
| `data_topic` | General topic data | ❌ **Not in template** |
|
||||
| `app1` | Approver 1 name | ❌ **Not in template** |
|
||||
| `app1_position` | Approver 1 position | ❌ **Not in template** |
|
||||
| `app2` | Approver 2 name | ❌ **Not in template** |
|
||||
| `app2_position` | Approver 2 position | ❌ **Not in template** |
|
||||
| `app3` | Approver 3 name | ❌ **Not in template** |
|
||||
| `app3_position` | Approver 3 position | ❌ **Not in template** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical Issues
|
||||
|
||||
### 3.1 Issue: Missing Exclusion Data Mapping
|
||||
|
||||
**Severity**: HIGH
|
||||
**Placeholder**: `exclusion_data`
|
||||
**Template Format**: `[[\"{exclusion_data}\"]]` (table with single column)
|
||||
**Expected Data Type**: `string[][]`
|
||||
**Current Status**: No mapping exists
|
||||
|
||||
**Problem**:
|
||||
- Template expects exclusion topic items to be displayed as a table
|
||||
- Legacy implementation used `formatTopicItems()` to convert topic items to `string[][]` format
|
||||
- Current implementation has no mapping for this placeholder
|
||||
- Exclusion topic data exists in database but is not passed to PDFME
|
||||
|
||||
**Impact**:
|
||||
- Exclusion section will be empty or show placeholder text
|
||||
- Critical business information missing from quotation PDF
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
// Legacy: src/features/quotations/lib/pdfme-export.ts
|
||||
function formatTopicItems(topic: any): string[][] {
|
||||
if (!topic || !topic.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item: any) => [item.content || '-']);
|
||||
}
|
||||
|
||||
const termsTopic = topics.find(t => t.topicType === 'exclusion_from_scope_of_supply');
|
||||
data.exclusion_data = formatTopicItems(termsTopic);
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add mapping for `exclusion_data` placeholder
|
||||
2. Transform topic items to `string[][]` format
|
||||
3. Handle empty topics with fallback `[['-']]`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Issue: Missing Topic Sections
|
||||
|
||||
**Severity**: HIGH
|
||||
**Affected Placeholders**:
|
||||
- `Terms_of_payment`, `data_term_of_payment`
|
||||
- `warranty`, `data_warranty`
|
||||
- `Delivery`, `data_delivery`
|
||||
- `Affter service`, `data_affter_service`
|
||||
|
||||
**Problem**:
|
||||
- Legacy template had separate sections for each topic type
|
||||
- Current template only has exclusion section
|
||||
- Payment, warranty, delivery, and after-service topics are not displayed
|
||||
- These are critical business terms in quotations
|
||||
|
||||
**Impact**:
|
||||
- Payment terms not visible to customer
|
||||
- Warranty information missing
|
||||
- Delivery details not specified
|
||||
- After-service information absent
|
||||
|
||||
**Legacy Structure**:
|
||||
```
|
||||
Topic: Scope of Work
|
||||
[data_scope]
|
||||
|
||||
Topic: Exclusion from scope of Supply:
|
||||
[data_exclusion]
|
||||
|
||||
Terms of payment upon progress of each item:
|
||||
[data_term_of_payment]
|
||||
|
||||
Warranty:
|
||||
[data_warranty]
|
||||
|
||||
Delivery:
|
||||
[data_delivery]
|
||||
|
||||
Affter service:
|
||||
[data_affter_service]
|
||||
```
|
||||
|
||||
**Current Template Structure**:
|
||||
```
|
||||
Quotation details...
|
||||
|
||||
Exclusion from scope of Supply:
|
||||
[exclusion_data]
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add topic section fields to template OR
|
||||
2. Merge all topics into a single table with labels
|
||||
3. Implement topic filtering based on product type
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Issue: Currency Format Mismatch
|
||||
|
||||
**Severity**: MEDIUM
|
||||
**Placeholder**: `quotation_price`
|
||||
**Template Usage**: `[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]`
|
||||
**Expected Type**: Formatted string (e.g., "฿1,000,000.00")
|
||||
**Actual Type**: Number (from database)
|
||||
|
||||
**Problem**:
|
||||
- Database stores `quotation.totalAmount` as number (double precision)
|
||||
- PDFME table cell expects formatted currency string
|
||||
- Current mapping likely passes raw number without formatting
|
||||
- Thai locale formatting required (฿ symbol, thousand separators, 2 decimal places)
|
||||
|
||||
**Impact**:
|
||||
- Price may display as "1000000" instead of "฿1,000,000.00"
|
||||
- Unprofessional appearance
|
||||
- Potential customer confusion
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
function formatCurrency(amount: number, currencyCode: CurrencyCode): string {
|
||||
const symbol = getCurrencySymbol(currencyCode);
|
||||
const formatted = new Intl.NumberFormat('th-TH', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
|
||||
data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add currency formatting transform
|
||||
2. Use `formatMask` in mapping or custom transform function
|
||||
3. Support multiple currency codes
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Issue: Date Format
|
||||
|
||||
**Severity**: MEDIUM
|
||||
**Placeholder**: `quotation_date`
|
||||
**Expected Format**: Localized date string (Thai: Buddhist calendar or English: Gregorian)
|
||||
**Current Format**: ISO date string
|
||||
|
||||
**Problem**:
|
||||
- Database stores dates as ISO 8601 strings (e.g., "2026-06-17T00:00:00.000Z")
|
||||
- PDF displays raw ISO string instead of formatted date
|
||||
- Legacy implementation had language-aware date formatting
|
||||
|
||||
**Impact**:
|
||||
- Unprofessional appearance
|
||||
- May cause confusion about quotation validity date
|
||||
- Doesn't match business requirements for Thai/English date formats
|
||||
|
||||
**Legacy Implementation**:
|
||||
```typescript
|
||||
function formatDate(dateString: string, language: 'th' | 'en'): string {
|
||||
const date = new Date(dateString);
|
||||
if (language === 'th') {
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
calendar: 'buddhist'
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fix**:
|
||||
1. Add date formatting transform
|
||||
2. Support Thai and English locales
|
||||
3. Configure format mask in mapping
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Issue: Missing Product Type Topic Mapping
|
||||
|
||||
**Severity**: HIGH
|
||||
**Legacy Behavior**: Different topic types mapped based on quotation product type
|
||||
|
||||
**Problem**:
|
||||
- Legacy implementation had product-specific topic mappings:
|
||||
- **Crane**: scope_of_work, exclusion_from_scope_of_supply, terms_of_payment_upon_progress_of_each_item, delivery_date, warranty
|
||||
- **Dockdoor**: dock_scope_of_work, exclusion_of_supply_installation, payment_conditions, delivery_date, warranty
|
||||
- **Solarcell**: solarcell_scope_of_work, solarcell_exclusion_of_supply_installation, solarcell_payment_conditions, solarcell_delivery, solarcell_warranty
|
||||
- Current implementation treats all topics uniformly
|
||||
- Topic type labels may not match product type
|
||||
|
||||
**Impact**:
|
||||
- Wrong topic labels for different product types
|
||||
- Inconsistent quotation formatting
|
||||
- Business process violation
|
||||
|
||||
**Required Fix**:
|
||||
1. Implement product type to topic type mapping
|
||||
2. Apply correct labels based on quotation type
|
||||
3. Support dynamic topic resolution
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Format Analysis
|
||||
|
||||
### 4.1 Table Data Formats
|
||||
|
||||
#### Exclusion Data Table
|
||||
|
||||
**Template Content**:
|
||||
```json
|
||||
"content": "[[\"{exclusion_data}\"]]"
|
||||
```
|
||||
|
||||
**Expected Format**: `string[][]`
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
[
|
||||
["Provide crane and installation service"],
|
||||
["Site survey and installation planning"],
|
||||
["Operator training (2 days)"]
|
||||
]
|
||||
```
|
||||
|
||||
**Legacy Transform**:
|
||||
```typescript
|
||||
function formatTopicItems(topic: any): string[][] {
|
||||
if (!topic || !topic.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item: any) => [item.content || '-']);
|
||||
}
|
||||
```
|
||||
|
||||
**Current Status**: ❌ Not implemented
|
||||
|
||||
---
|
||||
|
||||
#### Price Table
|
||||
|
||||
**Template Content**:
|
||||
```json
|
||||
"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]"
|
||||
```
|
||||
|
||||
**Expected Format**: `string[][]` (4 columns)
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
[
|
||||
["Price (Exclude VAT)", "฿1,000,000.00", " ", "THB"]
|
||||
]
|
||||
```
|
||||
|
||||
**Legacy Transform**:
|
||||
```typescript
|
||||
data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);
|
||||
data.currency = quotation.currency;
|
||||
```
|
||||
|
||||
**Current Status**: ⚠️ Number passed instead of formatted string
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Text Data Formats
|
||||
|
||||
#### Customer Contact Information
|
||||
|
||||
**Legacy Prefixes**:
|
||||
- Tel: `Tel : {customer_tel}`
|
||||
- Email: `Email : {customer_email}`
|
||||
|
||||
**Current Template**: Labels and data are separate fields
|
||||
- Label: "Tel" (static)
|
||||
- Data: `{customer_tel}` (placeholder)
|
||||
|
||||
**Status**: ✅ OK (modern approach)
|
||||
|
||||
---
|
||||
|
||||
## 5. Current Implementation Gaps
|
||||
|
||||
### 5.1 Missing Transform Functions
|
||||
|
||||
| Function | Purpose | Current Status |
|
||||
|----------|---------|----------------|
|
||||
| `formatTopicItems()` | Convert topic items to `string[][]` | ❌ Missing |
|
||||
| `formatCurrency()` | Format currency with locale | ⚠️ May exist but not applied |
|
||||
| `formatDate()` | Format date with locale | ⚠️ May exist but not applied |
|
||||
| `getCurrencySymbol()` | Get currency symbol | ❌ Missing |
|
||||
| `section()` | Create label/data pairs for topics | ❌ Missing |
|
||||
| `productTypeTopicMapping` | Map product type to topic types | ❌ Missing |
|
||||
|
||||
### 5.2 Missing Database Mappings
|
||||
|
||||
The following mappings should exist in `crm_document_template_mappings` table:
|
||||
|
||||
| placeholder_key | source_path | data_type | format_mask | status |
|
||||
|-----------------|-------------|-----------|-------------|--------|
|
||||
| `exclusion_data` | `topics.exclusion_from_scope_of_supply.items` | table | - | ❌ Missing |
|
||||
| `quotation_price` | `quotation.totalAmount` | scalar | currency_THB | ⚠️ Needs format |
|
||||
| `quotation_date` | `quotation.quotationDate` | scalar | date_th | ⚠️ Needs format |
|
||||
|
||||
### 5.3 Data Builder Issues
|
||||
|
||||
**File**: `src/features/crm/quotations/document/server/service.ts`
|
||||
|
||||
**Issues**:
|
||||
1. `buildQuotationDocumentData()` does not transform topic items to `string[][]`
|
||||
2. Does not format currency or dates
|
||||
3. Does not apply product type topic mapping
|
||||
4. Does not handle empty topics with fallbacks
|
||||
|
||||
---
|
||||
|
||||
## 6. Comparison with Legacy
|
||||
|
||||
### 6.1 Legacy Flow
|
||||
|
||||
```typescript
|
||||
generateQuotationPDF(quotation, customer, topics)
|
||||
↓
|
||||
mapDataToTemplate() // Direct mapping with transforms
|
||||
↓
|
||||
formatDate() // Thai/English date
|
||||
↓
|
||||
formatCurrency() // Thai locale currency
|
||||
↓
|
||||
formatTopicItems() // Convert to string[][]
|
||||
↓
|
||||
generateExtraTopicFields() // Dynamic schema
|
||||
↓
|
||||
pdfme.generate(template, inputs)
|
||||
```
|
||||
|
||||
### 6.2 Current Flow
|
||||
|
||||
```typescript
|
||||
buildQuotationDocumentData() // Fetch data
|
||||
↓
|
||||
mapDocumentDataToTemplateInput() // Generic mapping
|
||||
↓
|
||||
applyFormatMask() // Simple format masks only
|
||||
↓
|
||||
pdfme.generate(template, inputs)
|
||||
```
|
||||
|
||||
### 6.3 Key Differences
|
||||
|
||||
| Aspect | Legacy | Current | Gap |
|
||||
|--------|--------|---------|-----|
|
||||
| Mapping Approach | Direct with inline transforms | Generic via database | Transform logic |
|
||||
| Topic Formatting | `string[][]` via `formatTopicItems()` | Unknown | Format mismatch |
|
||||
| Currency Formatting | `formatCurrency()` with locale | Basic format mask | Missing locale |
|
||||
| Date Formatting | `formatDate()` with locale | Basic format mask | Missing locale |
|
||||
| Product Type | Product-specific topic mapping | None | Wrong labels |
|
||||
| Dynamic Topics | `generateExtraTopicFields()` | Static schema | No extra topics |
|
||||
| Fallback Values | Inline defaults in mapping | `defaultValue` column | May not cover all cases |
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommended Fixes
|
||||
|
||||
### 7.1 Immediate Fixes (P0)
|
||||
|
||||
#### Fix 1: Add Exclusion Data Mapping
|
||||
|
||||
**Add to `crm_document_template_mappings`**:
|
||||
```sql
|
||||
INSERT INTO crm_document_template_mappings (
|
||||
id, organization_id, template_version_id, placeholder_key,
|
||||
source_path, data_type, sort_order
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
'<organization_id>',
|
||||
'<template_version_id>',
|
||||
'exclusion_data',
|
||||
'topics.exclusion_from_scope_of_supply.items',
|
||||
'table',
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
**Add transform function** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatTopicItems(
|
||||
topic: { items?: Array<{ content?: string }> } | undefined
|
||||
): string[][] {
|
||||
if (!topic?.items || topic.items.length === 0) {
|
||||
return [['-']];
|
||||
}
|
||||
return topic.items.map((item) => [item.content || '-']);
|
||||
}
|
||||
```
|
||||
|
||||
**Update `buildQuotationDocumentData()`**:
|
||||
```typescript
|
||||
const exclusionTopic = topics.find(
|
||||
t => t.topicType === 'exclusion_from_scope_of_supply'
|
||||
);
|
||||
data.exclusion_data = formatTopicItems(exclusionTopic);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Fix 2: Format Currency
|
||||
|
||||
**Update mapping for `quotation_price`**:
|
||||
```sql
|
||||
UPDATE crm_document_template_mappings
|
||||
SET format_mask = 'currency_THB'
|
||||
WHERE placeholder_key = 'quotation_price';
|
||||
```
|
||||
|
||||
**Add currency formatter** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currencyCode: string = 'THB'
|
||||
): string {
|
||||
const symbols: Record<string, string> = {
|
||||
THB: '฿',
|
||||
USD: '$',
|
||||
EUR: '€'
|
||||
};
|
||||
const symbol = symbols[currencyCode] || currencyCode;
|
||||
const formatted = new Intl.NumberFormat('th-TH', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
```
|
||||
|
||||
**Update `mapDocumentDataToTemplateInput()`** in `document-template/server/service.ts`:
|
||||
```typescript
|
||||
if (mapping.formatMask?.startsWith('currency_')) {
|
||||
const currencyCode = mapping.formatMask.replace('currency_', '');
|
||||
result[mapping.placeholderKey] = formatCurrency(
|
||||
rawValue as number,
|
||||
currencyCode
|
||||
);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Fix 3: Format Date
|
||||
|
||||
**Update mapping for `quotation_date`**:
|
||||
```sql
|
||||
UPDATE crm_document_template_mappings
|
||||
SET format_mask = 'date_th'
|
||||
WHERE placeholder_key = 'quotation_date';
|
||||
```
|
||||
|
||||
**Add date formatter** in `src/features/crm/quotations/document/server/utils.ts`:
|
||||
```typescript
|
||||
export function formatDate(
|
||||
dateString: string,
|
||||
language: 'th' | 'en' = 'th'
|
||||
): string {
|
||||
const date = new Date(dateString);
|
||||
if (language === 'th') {
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
calendar: 'buddhist'
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Update `mapDocumentDataToTemplateInput()`**:
|
||||
```typescript
|
||||
if (mapping.formatMask?.startsWith('date_')) {
|
||||
const language = mapping.formatMask.replace('date_', '') as 'th' | 'en';
|
||||
result[mapping.placeholderKey] = formatDate(
|
||||
rawValue as string,
|
||||
language
|
||||
);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Template Updates (P0)
|
||||
|
||||
#### Option A: Add Missing Topic Sections to Template
|
||||
|
||||
Add the following fields to the template schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "payment_label",
|
||||
"type": "table",
|
||||
"content": "[[\"Terms of payment upon progress of each item:\"]]",
|
||||
"position": { "x": 10, "y": 220 },
|
||||
"width": 190,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"name": "payment_data",
|
||||
"type": "table",
|
||||
"content": "[[\"{payment_data}\"]]",
|
||||
"position": { "x": 15, "y": 230 },
|
||||
"width": 185,
|
||||
"height": 7
|
||||
},
|
||||
{
|
||||
"name": "warranty_label",
|
||||
"type": "table",
|
||||
"content": "[[\"Warranty:\"]]",
|
||||
"position": { "x": 10, "y": 250 },
|
||||
"width": 190,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"name": "warranty_data",
|
||||
"type": "table",
|
||||
"content": "[[\"{warranty_data}\"]]",
|
||||
"position": { "x": 15, "y": 260 },
|
||||
"width": 185,
|
||||
"height": 7
|
||||
}
|
||||
```
|
||||
|
||||
#### Option B: Consolidate Topics into Single Table
|
||||
|
||||
Create a unified topics table with labels:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "topics_table",
|
||||
"type": "table",
|
||||
"content": "[{topics_data}]",
|
||||
"position": { "x": 10, "y": 195 },
|
||||
"width": 190,
|
||||
"height": 50
|
||||
}
|
||||
```
|
||||
|
||||
Where `topics_data` is a structured array:
|
||||
```typescript
|
||||
[
|
||||
["Exclusion from scope of Supply:", "Provide crane, Site survey"],
|
||||
["Terms of payment:", "30% deposit, 70% on delivery"],
|
||||
["Warranty:", "12 months parts and labor"],
|
||||
["Delivery:", "30 days after order confirmation"],
|
||||
["After service:", "Annual maintenance available"]
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.3 Product Type Mapping (P1)
|
||||
|
||||
**Create configuration** in `src/features/crm/quotations/document/config/topic-mapping.ts`:
|
||||
```typescript
|
||||
export const TOPIC_TYPE_MAPPING: Record<string, Record<string, string>> = {
|
||||
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'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Update `buildQuotationDocumentData()`** to use product type:
|
||||
```typescript
|
||||
const mapping = TOPIC_TYPE_MAPPING[quotation.productType] || TOPIC_TYPE_MAPPING.crane;
|
||||
const exclusionTopic = topics.find(t => t.topicType === mapping.exclusion);
|
||||
data.exclusion_data = formatTopicItems(exclusionTopic);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Risk Assessment
|
||||
|
||||
### 8.1 High Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing exclusion data | High customer confusion | 100% | Add mapping immediately |
|
||||
| Missing payment terms | Legal/compliance issue | 100% | Add to template |
|
||||
| Wrong currency format | Unprofessional appearance | 90% | Add currency formatter |
|
||||
| Wrong date format | Date confusion | 90% | Add date formatter |
|
||||
|
||||
### 8.2 Medium Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing product type mapping | Inconsistent formatting | 80% | Implement product mapping |
|
||||
| Missing warranty info | Customer expectations | 70% | Add to template |
|
||||
| Missing delivery info | Order confusion | 70% | Add to template |
|
||||
|
||||
### 8.3 Low Risk
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Missing signatures | Process compliance | 50% | Add approval workflow |
|
||||
| Static company info | Outdated info | 30% | Make dynamic |
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Recommendations
|
||||
|
||||
### 9.1 Unit Tests
|
||||
|
||||
Create tests for transform functions:
|
||||
```typescript
|
||||
// tests/features/crm/quotations/document/utils.test.ts
|
||||
describe('formatTopicItems', () => {
|
||||
it('should format topic items to string[][]', () => {
|
||||
const topic = {
|
||||
items: [
|
||||
{ content: 'Item 1' },
|
||||
{ content: 'Item 2' }
|
||||
]
|
||||
};
|
||||
expect(formatTopicItems(topic)).toEqual([
|
||||
['Item 1'],
|
||||
['Item 2']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback for empty topic', () => {
|
||||
expect(formatTopicItems(undefined)).toEqual([['-']]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 Integration Tests
|
||||
|
||||
Test end-to-end PDF generation:
|
||||
```typescript
|
||||
describe('Quotation PDF Generation', () => {
|
||||
it('should generate PDF with all fields populated', async () => {
|
||||
const quotation = await createTestQuotation();
|
||||
const pdf = await generateQuotationPDF(quotation.id);
|
||||
|
||||
// Verify placeholders are replaced
|
||||
expect(pdf).toContain('CUSTOMER NAME');
|
||||
expect(pdf).not.toContain('{customer_name}');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 Visual Regression Tests
|
||||
|
||||
Compare generated PDFs with legacy PDFs:
|
||||
1. Generate PDF from legacy implementation
|
||||
2. Generate PDF from current implementation
|
||||
3. Compare pixel-by-pixel
|
||||
4. Identify visual differences
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation Priority
|
||||
|
||||
### Phase 1: Critical Fixes (1-2 days)
|
||||
1. ✅ Add exclusion data mapping
|
||||
2. ✅ Implement `formatTopicItems()`
|
||||
3. ✅ Format currency
|
||||
4. ✅ Format dates
|
||||
5. ✅ Test with real quotation
|
||||
|
||||
### Phase 2: Template Updates (1 day)
|
||||
1. Add missing topic sections (payment, warranty, delivery)
|
||||
2. Update template layout
|
||||
3. Add mappings for new fields
|
||||
|
||||
### Phase 3: Product Type Logic (1 day)
|
||||
1. Implement product type mapping
|
||||
2. Update topic resolution
|
||||
3. Test different product types
|
||||
|
||||
### Phase 4: Enhancements (2-3 days)
|
||||
1. Dynamic topic schema generation
|
||||
2. Approval signature integration
|
||||
3. Visual regression testing
|
||||
|
||||
---
|
||||
|
||||
## 11. Conclusion
|
||||
|
||||
The current PDFME implementation has significant gaps compared to the legacy system, resulting in incomplete or incorrectly formatted quotation PDFs. The primary issues are:
|
||||
|
||||
1. **Missing topic mappings** (exclusion, payment, warranty, delivery)
|
||||
2. **Missing transform functions** (formatTopicItems, currency, date)
|
||||
3. **Template structure differences** (fewer sections)
|
||||
4. **No product type logic** (generic topic labels)
|
||||
|
||||
**Recommended Action**: Implement Phase 1 fixes immediately to restore basic functionality, then proceed with template updates and product type logic.
|
||||
|
||||
**Estimated Total Effort**: 5-7 days for complete resolution.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Placeholder Inventory
|
||||
|
||||
### A.1 Current Template Placeholders
|
||||
|
||||
```
|
||||
customer_name
|
||||
customer_addr
|
||||
customer_tel
|
||||
customer_email
|
||||
customer_att
|
||||
project_name
|
||||
site_location
|
||||
quotation_date
|
||||
quotation_code
|
||||
quotation_price
|
||||
currency
|
||||
exclusion_data
|
||||
```
|
||||
|
||||
### A.2 Legacy Template Placeholders (Not in Current)
|
||||
|
||||
```
|
||||
exclusion_label
|
||||
Terms_of_payment
|
||||
data_term_of_payment
|
||||
warranty
|
||||
data_warranty
|
||||
Delivery
|
||||
data_delivery
|
||||
Affter service
|
||||
data_affter_service
|
||||
topic
|
||||
data_topic
|
||||
app1
|
||||
app1_position
|
||||
app2
|
||||
app2_position
|
||||
app3
|
||||
app3_position
|
||||
```
|
||||
|
||||
### A.3 Mapped vs Unmapped
|
||||
|
||||
| Category | Total | Mapped | Unmapped | % Mapped |
|
||||
|----------|-------|--------|----------|----------|
|
||||
| Customer Info | 4 | 4 | 0 | 100% |
|
||||
| Quotation Info | 6 | 6 | 0 | 100% |
|
||||
| Topics | 1 | 0 | 1 | 0% |
|
||||
| Signatures | 6 | 0 | 6 | 0% |
|
||||
| **Total** | **17** | **10** | **7** | **59%** |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Data Source Reference
|
||||
|
||||
### B.1 Database Schema
|
||||
|
||||
**Customer**: `crm_customers`
|
||||
- `name` → `{customer_name}`
|
||||
- `address` → `{customer_addr}`
|
||||
- `phone` → `{customer_tel}`
|
||||
- `email` → `{customer_email}`
|
||||
|
||||
**Quotation**: `crm_quotations`
|
||||
- `attention` → `{customer_att}`
|
||||
- `projectName` → `{project_name}`
|
||||
- `projectLocation` → `{site_location}`
|
||||
- `quotationDate` → `{quotation_date}`
|
||||
- `code` → `{quotation_code}`
|
||||
- `totalAmount` → `{quotation_price}`
|
||||
- `currency` → `{currency}`
|
||||
|
||||
**Topics**: `crm_quotation_topics` + `crm_quotation_topic_items`
|
||||
- `topicType` → determines which topic
|
||||
- `items[].content` → topic content items
|
||||
|
||||
### B.2 Source Path Examples
|
||||
|
||||
| Placeholder | Source Path | Notes |
|
||||
|-------------|-------------|-------|
|
||||
| `customer_name` | `customer.name` | Direct mapping |
|
||||
| `customer_tel` | `customer.contacts[primary].phone` | Array access |
|
||||
| `quotation_price` | `quotation.totalAmount` | Number → string transform |
|
||||
| `exclusion_data` | `topics.exclusion_from_scope_of_supply.items` | Topic → string[][] transform |
|
||||
|
||||
---
|
||||
|
||||
**Report Generated**: 2026-06-17
|
||||
**Auditor**: AI Code Review Agent
|
||||
**Version**: 1.0
|
||||
Reference in New Issue
Block a user