19 KiB
PDFME Legacy Audit
1. Executive Summary
This audit compares the legacy PDFME quotation generation implementation with the current ALLA OS CRM vNext implementation. The investigation reveals significant gaps in placeholder mapping, data transformation, and topic handling that explain why the current system displays incomplete or incorrectly mapped data.
Key Finding: The legacy implementation uses a direct mapping approach with specialized transform functions for each field, while the current implementation relies on a generic mapping system that may not handle all edge cases correctly.
2. Legacy Files Inspected
2.1 Core Files
src/features/quotations/lib/pdfme-export.ts- Main PDFME generation logicsrc/features/quotations/lib/quotation-preview-utils.ts- Format utilitiessrc/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json- Template definitionsrc/features/quotations/types/quotation-preview.types.ts- Type definitions
2.2 Current Project Files
src/features/foundation/pdf-generator/server/service.ts- PDF generation servicesrc/features/crm/quotations/document/server/service.ts- Document data buildersrc/features/foundation/document-template/server/service.ts- Template mapping servicesrc/db/schema.ts- Database schema
3. Legacy PDFME Flow
3.1 Architecture
generateQuotationPDF()
↓
mapDataToTemplate()
↓
formatDate() / formatCurrency()
↓
formatTopicItems() / section() / section2()
↓
generateExtraTopicFields()
↓
generate() from @pdfme/generator
3.2 Key Characteristics
- Direct mapping: Each placeholder key is explicitly mapped in
mapDataToTemplate() - Inline transforms: Transform functions are applied inline during mapping
- Dynamic schema generation: Extra topics modify template schema at runtime
- Multi-branch support: Different templates for
allavsonvalla - Specialized formatting: Currency, date, and topic item formatting
3.3 Data Structure
{
customer: QuotationPreviewCustomer
quotation: QuotationPreviewData
items: QuotationPreviewItem[]
topics: Array<{ topicType: string; items: Array<{ content: string }> }>
}
4. Legacy Placeholder Inventory
| Placeholder | Legacy Source | Transform | Current Mapping | Status |
|---|---|---|---|---|
customer_name |
customer.name |
Fallback to '-' |
✅ Mapped | OK |
customer_addr |
customer.address |
Fallback to '-' |
✅ Mapped | OK |
customer_tel |
customer.contacts[primary].mobile/phone |
Prefix: Tel : |
✅ Mapped | OK |
customer_email |
customer.contacts[primary].email |
Prefix: Email : |
✅ Mapped | OK |
customer_att |
quotation.attention |
Direct | ✅ Mapped | OK |
project_name |
quotation.projectName |
Direct | ✅ Mapped | OK |
site_location |
quotation.projectLocation |
Direct | ✅ Mapped | OK |
quotation_date |
quotation.quotationDate |
formatDate(date, 'en') |
✅ Mapped | ⚠️ Check format |
quotation_code |
quotation.code |
Direct | ✅ Mapped | OK |
quotation_price |
quotation.totalAmount |
formatCurrency(amount, currency) |
✅ Mapped | ⚠️ Check format |
currency |
quotation.currency |
Direct | ✅ Mapped | OK |
exclusion_label |
termsTopic.topicType |
Hardcoded: "Exclusion from scope of Supply:" |
❌ Missing | MISSING |
exclusion_data |
termsTopic.items |
formatTopicItems() → string[][] |
❌ Missing | MISSING |
Terms_of_payment |
termsTopic.topicType |
Hardcoded: "Terms of payment upon progress of each item:" |
❌ Missing | MISSING |
data_term_of_payment |
termsTopic.items |
formatTopicItems() → string[][] |
❌ Missing | MISSING |
warranty |
warrantyTopic.topicType |
Hardcoded: "Warranty:" |
❌ Missing | MISSING |
data_warranty |
warrantyTopic.items |
formatTopicItems() → string[][] |
❌ Missing | MISSING |
Delivery |
deliveryTopic.topicType |
Hardcoded: "Delivery:" |
❌ Missing | MISSING |
data_delivery |
deliveryTopic.items |
formatTopicItems() → string[][] |
❌ Missing | MISSING |
Affter service |
deliveryTopic.topicType |
Hardcoded: "Affter service:" |
❌ Missing | MISSING |
data_affter_service |
deliveryTopic.items |
formatTopicItems() → string[][] |
❌ Missing | MISSING |
topic |
Static text | Hardcoded: "Topic:" |
✅ Static | OK |
data_topic |
topics |
Complex transform needed | ❌ Missing | MISSING |
item_topic |
Not in legacy | - | ❌ Not defined | TEMPLATE MISMATCH |
app1 |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
app1_position |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
app2 |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
app2_position |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
app3 |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
app3_position |
Not populated | Static: "Type Something..." |
❌ Missing | MISSING |
5. Legacy Transform Functions
5.1 formatDate(dateString: string, language: 'th' | 'en'): string
Purpose: Format date for PDF display Input: ISO date string Output: Localized date string (Thai: Buddhist calendar, English: Gregorian) Why it matters: PDF requires specific date format Current project: Has equivalent but may differ in format
// Legacy
export 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'
});
}
5.2 formatCurrency(amount: number, currencyCode: CurrencyCode): string
Purpose: Format currency with Thai locale Input: Amount and currency code Output: Formatted string (e.g., "฿1,000,000.00") Why it matters: PDF price display requires proper formatting Current project: May have equivalent but needs verification
// Legacy
export 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}`;
}
5.3 formatTopicItems(topic: any): string[][]
Purpose: Transform topic items to PDFME table format
Input: Topic object with items array
Output: 2D array where each item is wrapped in array
Why it matters: PDFME tables require string[][] format
Current project: ❌ MISSING
// Legacy
function formatTopicItems(topic: any): string[][] {
if (!topic || !topic.items || topic.items.length === 0) {
return [['-']];
}
return topic.items.map((item: any) => [item.content || '-']);
}
5.4 section(labelKey: string, valueKey: string, topic: any, label: string)
Purpose: Create label and data table pairs
Input: Label key, value key, topic data, label text
Output: Object with two keys mapping to string[][]
Why it matters: Handles topic sections with consistent label/data pattern
Current project: ❌ MISSING
// Legacy
function section(labelKey: string, valueKey: string, topic: any, label: string) {
if (!topic) return {};
return {
[labelKey]: [[label]],
[valueKey]: formatTopicItems(topic)
};
}
5.5 section2(labelKey: string, valueKey: string, topic: any, label: string)
Purpose: Handle extra topics (unmapped/dynamic topics)
Input: Same as section but supports arrays
Output: Object with numbered keys for multiple topics
Why it matters: Allows unlimited extra topics with dynamic numbering
Current project: ❌ MISSING
5.6 generateExtraTopicFields(extraTopics: any[]): any[]
Purpose: Dynamically generate PDFME schema fields for extra topics Input: Array of extra topics Output: Array of PDFME field definitions Why it matters: Allows extra topics to appear in PDF without template modification Current project: ❌ MISSING
6. Legacy Table Data Format
6.1 Topic Tables (Scope, Exclusion, Payment, Warranty, Delivery)
Expected Format: string[][]
- Each row is an array of strings
- Each item becomes:
[item.content] - Empty topic becomes:
[["-"]]
Example:
// Input
{
topicType: "scope",
items: [
{ content: "Provide crane" },
{ content: "Install at site" }
]
}
// Output
[
["Provide crane"],
["Install at site"]
]
6.2 Price Table
Format: [[string, string, string, string]]
- Hardcoded 4 columns
- Example:
[[\"Price (Exclude VAT)\",\"฿1,000,000.00\",\" \",\"THB\"]]
6.3 Current Project Table Handling
Issue: Current mapDocumentDataToTemplateInput() returns table data as:
- Array of objects for
dataType: 'table' - May not format as
string[][]correctly - May not handle single-column tables properly
7. Legacy Topic Handling
7.1 Product Type Mapping
Legacy maps quotation type to topic type keys:
const TOPIC_MAPPING = {
crane: {
scope: 'scope_of_work',
exclusion: 'exclusion_from_scope_of_supply',
terms: 'terms_of_payment_upon_progress_of_each_item',
delivery: 'delivery_date',
warranty: 'warranty'
},
dockdoor: {
scope: 'dock_scope_of_work',
exclusion: 'exclusion_of_supply_installation',
terms: 'payment_conditions',
delivery: 'delivery_date',
warranty: 'warranty'
},
solarcell: {
scope: 'solarcell_scope_of_work',
exclusion: 'solarcell_exclusion_of_supply_installation',
terms: 'solarcell_payment_conditions',
delivery: 'solarcell_delivery',
warranty: 'solarcell_warranty'
}
}
7.2 Topic Splitting Logic
Legacy splits topics into two categories:
- Mapped topics: scope, exclusion, terms, delivery, warranty (based on quotation type)
- Extra topics: All other topics (dynamic)
7.3 Topic Processing
- Mapped topics use
section()helper - Extra topics use
section2()helper with numbering - Extra topics generate dynamic PDFME schema fields
7.4 Current Project Topic Handling
Issues:
- No product type mapping
- No topic splitting
- No dynamic schema generation
- Topics likely treated uniformly, losing type-specific labels
8. Legacy Approval / Signature Handling
8.1 Approval Fields in Template
Template has static signature fields:
app1,app1_positionapp2,app2_positionapp3,app3_position
8.2 Legacy Population
Status: Not populated in legacy code
- Fields contain static placeholder:
"Type Something..." - Appears to be manually filled or signature-only
- No dynamic data source
8.3 Current Project
Status: ❌ NOT MAPPED
- No mapping for approval fields
- No source data for signatures
- Approval data exists but not connected to PDF
9. Current Project Gaps
9.1 Critical Missing Placeholders
- Topic sections (scope, exclusion, terms, warranty, delivery)
- Extra topics (dynamic topics)
- Approval signatures
9.2 Missing Transform Functions
formatTopicItems()- converts topic items tostring[][]section()- creates label/data table pairssection2()- handles extra topics with numberinggenerateExtraTopicFields()- dynamic schema generation
9.3 Data Shape Mismatches
- Tables: Legacy expects
string[][], current may return object arrays - Price table: Legacy uses hardcoded 4-column format
- Topic items: Legacy uses
.contentfield, current may differ
9.4 Missing Product Type Logic
- No product type to topic type mapping
- All topics treated the same way
- Type-specific labels not applied
9.5 Missing Dynamic Schema Generation
- Extra topics cannot appear in PDF
- Template schema is static
- Cannot handle variable number of topics
9.6 Template Mismatch
- Current template has
item_topicplaceholder - Legacy uses
data_topicwith different logic - May need template versioning or migration
10. Recommended Fix Plan
10.1 Phase 1: Add Missing Transform Functions
Priority: HIGH
Create utility functions in src/features/crm/quotations/document/server/utils.ts:
export function formatTopicItems(topic: { items?: Array<{ content?: string }> }): string[][] {
if (!topic?.items?.length) {
return [['-']];
}
return topic.items.map((item) => [item.content || '-']);
}
export function createTopicSection(
label: string,
topic: { items?: Array<{ content?: string }> }
): Record<string, string[][]> {
return {
[`${label}_label`]: [[label]],
[`${label}_data`]: formatTopicItems(topic)
};
}
10.2 Phase 2: Add Product Type Mapping
Priority: HIGH
Create mapping configuration:
// src/features/crm/quotations/document/config/topic-mapping.ts
const 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_supply_installation',
payment: 'payment_conditions',
delivery: 'delivery',
warranty: 'warranty'
}
};
10.3 Phase 3: Update Document Data Builder
Priority: HIGH
Modify buildQuotationDocumentData() in document/server/service.ts:
- Add product type detection from quotation
- Split topics into mapped and extra categories
- Format topic items as
string[][] - Add approval/signature data
10.4 Phase 4: Fix Table Data Formatting
Priority: MEDIUM
Update mapDocumentDataToTemplateInput() in document-template/server/service.ts:
if (mapping.dataType === 'table') {
const rows = Array.isArray(rawValue) ? rawValue : [];
// Check if it's already string[][] format
if (rows.length > 0 && Array.isArray(rows[0])) {
result[mapping.placeholderKey] = rows;
} else {
// Convert objects to string[][]
result[mapping.placeholderKey] = rows.map((row) => {
const rowRecord = row as Record<string, unknown>;
return mapping.columns.reduce<string[]>((acc, col) => {
acc.push(String(applyFormatMask(rowRecord[col.sourceField], col.formatMask) || ''));
return acc;
}, []);
});
});
}
continue;
}
10.5 Phase 5: Update Template Mappings
Priority: HIGH
Add mappings for missing placeholders in database:
exclusion_label- static textexclusion_data- topic data as tableTerms_of_payment- static textdata_term_of_payment- topic data as tablewarranty- static textdata_warranty- topic data as tableDelivery- static textdata_delivery- topic data as tableAffter service- static textdata_affter_service- topic data as tabledata_topic- general topic data- Approval fields
10.6 Phase 6: Add Approval Signature Data
Priority: MEDIUM
Extract approval workflow data and map to signature fields:
app1= Sales Manager (from approval timeline)app2= Department Manager (from approval timeline)app3= Top Manager (from approval timeline)- Position fields from user profiles or roles
10.7 Phase 7: Template Verification
Priority: HIGH
Verify current template matches legacy:
- Check all placeholder names match
- Verify field positions match
- Ensure font names match (
cordia,cordiaBold) - Validate table structures
10.8 Phase 8: Dynamic Schema Generation
Priority: LOW (Future Enhancement)
Implement dynamic schema generation for extra topics:
- Generate PDFME field definitions
- Inject into template schema
- Calculate positioning dynamically
11. Risk / Notes
11.1 Risks
- Template Compatibility: Current template may have different placeholder names or structure
- Data Migration: Existing quotations may not have topic data in correct format
- Backward Compatibility: Changes may break existing PDF generation
- Font Issues: Cordia fonts may not be available in production
11.2 Notes
- Legacy Template Location:
- ALLA:
src/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json - ONVALLA:
src/features/quotations/pdfme/ONVALLA_template_pdfme_fainal3.json
- ALLA:
- Current Template:
src/pdfme_template/ALLA_template_pdfme_fainal3.json - Template Version: Current template may be different version from legacy
- Topic Data Model: Current topics structure may need adjustment to match legacy expectations
- Approval Data: Current approval system is more complex than legacy, need to map correctly
11.3 Recommendations
- Create a migration script to convert existing topic data to new format
- Implement comprehensive PDF preview to verify all fields
- Add unit tests for each transform function
- Document all placeholder mappings in a central configuration
- Consider versioning PDFME templates to handle changes
12. Conclusion
The legacy PDFME implementation uses a specialized, direct mapping approach with inline transforms that ensure correct data formatting. The current implementation uses a more generic mapping system that, while more flexible, is missing critical transform logic and placeholder mappings.
Primary Issues:
- Missing topic section mappings (scope, exclusion, terms, warranty, delivery)
- Missing
formatTopicItems()transform function - No support for extra topics
- No product type to topic type mapping
- Template placeholder mismatch (
item_topicvsdata_topic) - Missing approval signature mappings
Estimated Effort: 2-3 days to implement all fixes and test thoroughly.
Next Steps:
- Implement transform functions (Phase 1)
- Add topic mappings (Phase 2-3)
- Update template mappings in database (Phase 5)
- Test with real quotation data
- Compare output with legacy PDFs