mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 04:17:37 +00:00
150 lines
6.2 KiB
TypeScript
150 lines
6.2 KiB
TypeScript
import ExcelJS from 'exceljs';
|
|
|
|
// Brand palette — matches finance-workbook.ts / ActionButton's primary variant, kept as a
|
|
// small local copy rather than a shared import since these are two unrelated export domains.
|
|
const BRAND = 'FF14714C';
|
|
const BRAND_TINT = 'FFEAF5EF';
|
|
const INK = 'FF1F2937';
|
|
const MUTED = 'FF6B7280';
|
|
const BORDER = 'FFE2E5E1';
|
|
const WHITE = 'FFFFFFFF';
|
|
|
|
const THIN_BORDER: Partial<ExcelJS.Borders> = {
|
|
top: { style: 'thin', color: { argb: BORDER } },
|
|
left: { style: 'thin', color: { argb: BORDER } },
|
|
bottom: { style: 'thin', color: { argb: BORDER } },
|
|
right: { style: 'thin', color: { argb: BORDER } },
|
|
};
|
|
|
|
/** Column order is the contract — passenger-excel.ts reads by this same header order. */
|
|
export const PASSENGER_TEMPLATE_COLUMNS = [
|
|
'Full Name',
|
|
'Date of Birth (YYYY-MM-DD)',
|
|
'Passenger Type',
|
|
'ID Document Type',
|
|
'ID Document Number',
|
|
'Passport Number',
|
|
'Passport Country',
|
|
'Nationality',
|
|
'Phone',
|
|
'Email',
|
|
] as const;
|
|
|
|
const REQUIRED_ROW = 200;
|
|
|
|
export interface PassengerTemplateInput {
|
|
trainNumber: string;
|
|
origin: string;
|
|
destination: string;
|
|
travelDate: string;
|
|
seatClassName: string;
|
|
adultCount: number;
|
|
childCount: number;
|
|
}
|
|
|
|
export async function buildPassengerTemplate(input: PassengerTemplateInput): Promise<Blob> {
|
|
const wb = new ExcelJS.Workbook();
|
|
wb.creator = 'EDR Passenger Backoffice';
|
|
wb.created = new Date();
|
|
|
|
const ws = wb.addWorksheet('Passengers', { views: [{ state: 'frozen', ySplit: 5 }] });
|
|
ws.columns = PASSENGER_TEMPLATE_COLUMNS.map((h) => ({ width: h.length < 14 ? 18 : h.length + 4 }));
|
|
|
|
// ── Title + trip context banner ──────────────────────────────────────────
|
|
ws.mergeCells(1, 1, 1, PASSENGER_TEMPLATE_COLUMNS.length);
|
|
const title = ws.getCell(1, 1);
|
|
title.value = 'EDR Group Booking — Passenger Template';
|
|
title.font = { bold: true, size: 16, color: { argb: WHITE } };
|
|
title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND } };
|
|
title.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
|
ws.getRow(1).height = 30;
|
|
for (let c = 1; c <= PASSENGER_TEMPLATE_COLUMNS.length; c++) ws.getCell(1, c).fill = title.fill;
|
|
|
|
ws.mergeCells(2, 1, 2, PASSENGER_TEMPLATE_COLUMNS.length);
|
|
const subtitle = ws.getCell(2, 1);
|
|
subtitle.value = `Train ${input.trainNumber} · ${input.origin} → ${input.destination} · ${input.travelDate} · ${input.seatClassName}`;
|
|
subtitle.font = { size: 11, color: { argb: INK } };
|
|
subtitle.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
|
ws.getRow(2).height = 20;
|
|
|
|
ws.mergeCells(3, 1, 3, PASSENGER_TEMPLATE_COLUMNS.length);
|
|
const requirement = ws.getCell(3, 1);
|
|
const total = input.adultCount + input.childCount;
|
|
requirement.value = `Fill in exactly ${total} passenger row${total === 1 ? '' : 's'} below — ${input.adultCount} Adult${input.adultCount === 1 ? '' : 's'} + ${input.childCount} Child${input.childCount === 1 ? '' : 'ren'}. One row per passenger, in any order.`;
|
|
requirement.font = { italic: true, size: 10, color: { argb: MUTED } };
|
|
requirement.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
|
ws.getRow(3).height = 18;
|
|
|
|
ws.mergeCells(4, 1, 4, PASSENGER_TEMPLATE_COLUMNS.length);
|
|
const instructions = ws.getCell(4, 1);
|
|
instructions.value =
|
|
'Columns marked * are required. Date of Birth must be YYYY-MM-DD and not in the future — it determines Adult/Child pricing (under 5 = Child). ' +
|
|
'ID Document Type must be one of: NATIONAL_ID, PASSPORT, DRIVING_LICENSE, OTHER. Do not rename or reorder columns.';
|
|
instructions.font = { size: 9, color: { argb: MUTED } };
|
|
instructions.alignment = { vertical: 'middle', horizontal: 'left', indent: 1, wrapText: true };
|
|
ws.getRow(4).height = 28;
|
|
|
|
// ── Header row ────────────────────────────────────────────────────────────
|
|
const headerRow = ws.getRow(5);
|
|
const requiredCols = new Set([0, 1, 2, 3]); // Full Name, DOB, Passenger Type, ID Document Type
|
|
PASSENGER_TEMPLATE_COLUMNS.forEach((h, i) => {
|
|
const cell = headerRow.getCell(i + 1);
|
|
cell.value = requiredCols.has(i) ? `${h} *` : h;
|
|
cell.font = { bold: true, color: { argb: WHITE }, size: 11 };
|
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND } };
|
|
cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
|
|
cell.border = THIN_BORDER;
|
|
});
|
|
headerRow.height = 30;
|
|
|
|
// ── One filled example row so the format is obvious at a glance ──────────
|
|
const example = ws.getRow(6);
|
|
const exampleValues = [
|
|
'Abebe Kebede',
|
|
'1990-05-15',
|
|
'Adult',
|
|
'NATIONAL_ID',
|
|
'ET123456789',
|
|
'',
|
|
'',
|
|
'Ethiopian',
|
|
'+251911234567',
|
|
'abebe@example.com',
|
|
];
|
|
exampleValues.forEach((v, i) => {
|
|
const cell = example.getCell(i + 1);
|
|
cell.value = v;
|
|
cell.font = { italic: true, color: { argb: MUTED } };
|
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND_TINT } };
|
|
cell.border = THIN_BORDER;
|
|
});
|
|
|
|
// ── Blank rows with borders + dropdown validation for Passenger Type / ID Document Type ──
|
|
// exceljs's types only expose per-cell `cell.dataValidation`, not a worksheet-level range API.
|
|
for (let r = 7; r <= REQUIRED_ROW; r++) {
|
|
const row = ws.getRow(r);
|
|
for (let c = 1; c <= PASSENGER_TEMPLATE_COLUMNS.length; c++) {
|
|
row.getCell(c).border = THIN_BORDER;
|
|
}
|
|
row.getCell(3).dataValidation = {
|
|
type: 'list',
|
|
allowBlank: true,
|
|
formulae: ['"Adult,Child"'],
|
|
showErrorMessage: true,
|
|
errorTitle: 'Invalid Passenger Type',
|
|
error: 'Choose Adult or Child.',
|
|
};
|
|
row.getCell(4).dataValidation = {
|
|
type: 'list',
|
|
allowBlank: true,
|
|
formulae: ['"NATIONAL_ID,PASSPORT,DRIVING_LICENSE,OTHER"'],
|
|
showErrorMessage: true,
|
|
errorTitle: 'Invalid ID Document Type',
|
|
error: 'Choose NATIONAL_ID, PASSPORT, DRIVING_LICENSE, or OTHER.',
|
|
};
|
|
}
|
|
|
|
const buffer = await wb.xlsx.writeBuffer();
|
|
return new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
}
|