910 lines
26 KiB
Markdown
910 lines
26 KiB
Markdown
# 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 |