Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m53s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Has been cancelled
- Replace all ../../domain/ imports with @domain/ across 67 files - Configure NestJS to use tsconfig.build.json with rootDir - Add tsc-alias to resolve path aliases after build - This fixes 'Cannot find module' TypeScript compilation errors Fixed files: - 30 files in application layer - 37 files in infrastructure layer
259 lines
7.7 KiB
TypeScript
259 lines
7.7 KiB
TypeScript
/**
|
|
* 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<string, any> {
|
|
const { booking, rateQuote } = data;
|
|
const result: Record<string, any> = {};
|
|
|
|
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, string> = {
|
|
[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;
|
|
}
|
|
}
|