# 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 logic - **`src/features/quotations/lib/quotation-preview-utils.ts`** - Format utilities - **`src/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json`** - Template definition - **`src/features/quotations/types/quotation-preview.types.ts`** - Type definitions ### 2.2 Current Project Files - **`src/features/foundation/pdf-generator/server/service.ts`** - PDF generation service - **`src/features/crm/quotations/document/server/service.ts`** - Document data builder - **`src/features/foundation/document-template/server/service.ts`** - Template mapping service - **`src/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 `alla` vs `onvalla` - **Specialized formatting**: Currency, date, and topic item formatting ### 3.3 Data Structure ```typescript { 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 ```typescript // 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 ```typescript // 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** ```typescript // 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** ```typescript // 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**: ```typescript // 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: ```typescript 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: 1. **Mapped topics**: scope, exclusion, terms, delivery, warranty (based on quotation type) 2. **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_position` - `app2`, `app2_position` - `app3`, `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 1. **Topic sections** (scope, exclusion, terms, warranty, delivery) 2. **Extra topics** (dynamic topics) 3. **Approval signatures** ### 9.2 Missing Transform Functions 1. `formatTopicItems()` - converts topic items to `string[][]` 2. `section()` - creates label/data table pairs 3. `section2()` - handles extra topics with numbering 4. `generateExtraTopicFields()` - dynamic schema generation ### 9.3 Data Shape Mismatches 1. **Tables**: Legacy expects `string[][]`, current may return object arrays 2. **Price table**: Legacy uses hardcoded 4-column format 3. **Topic items**: Legacy uses `.content` field, 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_topic` placeholder - Legacy uses `data_topic` with 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`: ```typescript 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 { return { [`${label}_label`]: [[label]], [`${label}_data`]: formatTopicItems(topic) }; } ``` ### 10.2 Phase 2: Add Product Type Mapping **Priority**: HIGH Create mapping configuration: ```typescript // 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`: 1. Add product type detection from quotation 2. Split topics into mapped and extra categories 3. Format topic items as `string[][]` 4. Add approval/signature data ### 10.4 Phase 4: Fix Table Data Formatting **Priority**: MEDIUM Update `mapDocumentDataToTemplateInput()` in `document-template/server/service.ts`: ```typescript 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; return mapping.columns.reduce((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: 1. `exclusion_label` - static text 2. `exclusion_data` - topic data as table 3. `Terms_of_payment` - static text 4. `data_term_of_payment` - topic data as table 5. `warranty` - static text 6. `data_warranty` - topic data as table 7. `Delivery` - static text 8. `data_delivery` - topic data as table 9. `Affter service` - static text 10. `data_affter_service` - topic data as table 11. `data_topic` - general topic data 12. 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: 1. Check all placeholder names match 2. Verify field positions match 3. Ensure font names match (`cordia`, `cordiaBold`) 4. Validate table structures ### 10.8 Phase 8: Dynamic Schema Generation **Priority**: LOW (Future Enhancement) Implement dynamic schema generation for extra topics: 1. Generate PDFME field definitions 2. Inject into template schema 3. Calculate positioning dynamically --- ## 11. Risk / Notes ### 11.1 Risks 1. **Template Compatibility**: Current template may have different placeholder names or structure 2. **Data Migration**: Existing quotations may not have topic data in correct format 3. **Backward Compatibility**: Changes may break existing PDF generation 4. **Font Issues**: Cordia fonts may not be available in production ### 11.2 Notes 1. **Legacy Template Location**: - ALLA: `src/features/quotations/pdfme/ALLA_template_pdfme_fainal3.json` - ONVALLA: `src/features/quotations/pdfme/ONVALLA_template_pdfme_fainal3.json` 2. **Current Template**: `src/pdfme_template/ALLA_template_pdfme_fainal3.json` 3. **Template Version**: Current template may be different version from legacy 4. **Topic Data Model**: Current topics structure may need adjustment to match legacy expectations 5. **Approval Data**: Current approval system is more complex than legacy, need to map correctly ### 11.3 Recommendations 1. Create a migration script to convert existing topic data to new format 2. Implement comprehensive PDF preview to verify all fields 3. Add unit tests for each transform function 4. Document all placeholder mappings in a central configuration 5. 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**: 1. Missing topic section mappings (scope, exclusion, terms, warranty, delivery) 2. Missing `formatTopicItems()` transform function 3. No support for extra topics 4. No product type to topic type mapping 5. Template placeholder mismatch (`item_topic` vs `data_topic`) 6. Missing approval signature mappings **Estimated Effort**: 2-3 days to implement all fixes and test thoroughly. **Next Steps**: 1. Implement transform functions (Phase 1) 2. Add topic mappings (Phase 2-3) 3. Update template mappings in database (Phase 5) 4. Test with real quotation data 5. Compare output with legacy PDFs