/** * Export Service * * Handles booking data export to various formats (CSV, Excel, JSON) */ import { Injectable, Logger } from '@nestjs/common'; import { Booking } from '@domain/entities/booking.entity'; import { RateQuote } from '@domain/entities/rate-quote.entity'; import { ExportFormat, ExportField } from '../dto/booking-export.dto'; import * as ExcelJS from 'exceljs'; interface BookingExportData { booking: Booking; rateQuote: RateQuote; } @Injectable() export class ExportService { private readonly logger = new Logger(ExportService.name); /** * Export bookings to specified format */ async exportBookings( data: BookingExportData[], format: ExportFormat, fields?: ExportField[] ): Promise<{ buffer: Buffer; contentType: string; filename: string }> { this.logger.log( `Exporting ${data.length} bookings to ${format} format with ${fields?.length || 'all'} fields` ); switch (format) { case ExportFormat.CSV: return this.exportToCSV(data, fields); case ExportFormat.EXCEL: return this.exportToExcel(data, fields); case ExportFormat.JSON: return this.exportToJSON(data, fields); default: throw new Error(`Unsupported export format: ${format}`); } } /** * Export to CSV format */ private async exportToCSV( data: BookingExportData[], fields?: ExportField[] ): Promise<{ buffer: Buffer; contentType: string; filename: string }> { const selectedFields = fields || Object.values(ExportField); const rows = data.map(item => this.extractFields(item, selectedFields)); // Build CSV header const header = selectedFields.map(field => this.getFieldLabel(field)).join(','); // Build CSV rows const csvRows = rows.map(row => selectedFields.map(field => this.escapeCSVValue(row[field] || '')).join(',') ); const csv = [header, ...csvRows].join('\n'); const buffer = Buffer.from(csv, 'utf-8'); const timestamp = new Date().toISOString().split('T')[0]; const filename = `bookings_export_${timestamp}.csv`; return { buffer, contentType: 'text/csv', filename, }; } /** * Export to Excel format */ private async exportToExcel( data: BookingExportData[], fields?: ExportField[] ): Promise<{ buffer: Buffer; contentType: string; filename: string }> { const selectedFields = fields || Object.values(ExportField); const rows = data.map(item => this.extractFields(item, selectedFields)); const workbook = new ExcelJS.Workbook(); workbook.creator = 'Xpeditis'; workbook.created = new Date(); const worksheet = workbook.addWorksheet('Bookings'); // Add header row with styling const headerRow = worksheet.addRow(selectedFields.map(field => this.getFieldLabel(field))); headerRow.font = { bold: true }; headerRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' }, }; // Add data rows rows.forEach(row => { const values = selectedFields.map(field => row[field] || ''); worksheet.addRow(values); }); // Auto-fit columns worksheet.columns.forEach(column => { let maxLength = 10; column.eachCell?.({ includeEmpty: false }, cell => { const columnLength = cell.value ? String(cell.value).length : 10; if (columnLength > maxLength) { maxLength = columnLength; } }); column.width = Math.min(maxLength + 2, 50); }); const buffer = await workbook.xlsx.writeBuffer(); const timestamp = new Date().toISOString().split('T')[0]; const filename = `bookings_export_${timestamp}.xlsx`; return { buffer: Buffer.from(buffer), contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', filename, }; } /** * Export to JSON format */ private async exportToJSON( data: BookingExportData[], fields?: ExportField[] ): Promise<{ buffer: Buffer; contentType: string; filename: string }> { const selectedFields = fields || Object.values(ExportField); const rows = data.map(item => this.extractFields(item, selectedFields)); const json = JSON.stringify( { exportedAt: new Date().toISOString(), totalBookings: rows.length, bookings: rows, }, null, 2 ); const buffer = Buffer.from(json, 'utf-8'); const timestamp = new Date().toISOString().split('T')[0]; const filename = `bookings_export_${timestamp}.json`; return { buffer, contentType: 'application/json', filename, }; } /** * Extract specified fields from booking data */ private extractFields(data: BookingExportData, fields: ExportField[]): Record { const { booking, rateQuote } = data; const result: Record = {}; fields.forEach(field => { switch (field) { case ExportField.BOOKING_NUMBER: result[field] = booking.bookingNumber.value; break; case ExportField.STATUS: result[field] = booking.status.value; break; case ExportField.CREATED_AT: result[field] = booking.createdAt.toISOString(); break; case ExportField.CARRIER: result[field] = rateQuote.carrierName; break; case ExportField.ORIGIN: result[field] = `${rateQuote.origin.name} (${rateQuote.origin.code})`; break; case ExportField.DESTINATION: result[field] = `${rateQuote.destination.name} (${rateQuote.destination.code})`; break; case ExportField.ETD: result[field] = rateQuote.etd.toISOString(); break; case ExportField.ETA: result[field] = rateQuote.eta.toISOString(); break; case ExportField.SHIPPER: result[field] = booking.shipper.name; break; case ExportField.CONSIGNEE: result[field] = booking.consignee.name; break; case ExportField.CONTAINER_TYPE: result[field] = booking.containers.map(c => c.type).join(', '); break; case ExportField.CONTAINER_COUNT: result[field] = booking.containers.length; break; case ExportField.TOTAL_TEUS: result[field] = booking.containers.reduce((total, c) => { return total + (c.type.startsWith('20') ? 1 : 2); }, 0); break; case ExportField.PRICE: result[field] = `${rateQuote.pricing.currency} ${rateQuote.pricing.totalAmount.toFixed( 2 )}`; break; } }); return result; } /** * Get human-readable field label */ private getFieldLabel(field: ExportField): string { const labels: Record = { [ExportField.BOOKING_NUMBER]: 'Booking Number', [ExportField.STATUS]: 'Status', [ExportField.CREATED_AT]: 'Created At', [ExportField.CARRIER]: 'Carrier', [ExportField.ORIGIN]: 'Origin', [ExportField.DESTINATION]: 'Destination', [ExportField.ETD]: 'ETD', [ExportField.ETA]: 'ETA', [ExportField.SHIPPER]: 'Shipper', [ExportField.CONSIGNEE]: 'Consignee', [ExportField.CONTAINER_TYPE]: 'Container Type', [ExportField.CONTAINER_COUNT]: 'Container Count', [ExportField.TOTAL_TEUS]: 'Total TEUs', [ExportField.PRICE]: 'Price', }; return labels[field]; } /** * Escape CSV value (handle commas, quotes, newlines) */ private escapeCSVValue(value: string): string { const stringValue = String(value); if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) { return `"${stringValue.replace(/"/g, '""')}"`; } return stringValue; } }