Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/util/InvoicesExcelReport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,79 @@ describe('util', () => {
});
});

describe('styling', () => {
const GRAY_FILL = {type: 'pattern', pattern: 'solid', fgColor: {argb: 'FFD9D9D9'}};
const NO_FILL = {type: 'pattern', pattern: 'none'};
const THIN_BLACK = {style: 'thin', color: {argb: 'FF000000'}};

it('fills the title row across the widest table\'s full width, regardless of title length', async () => {
// A short title still needs the full 12-column span, since the
// landing fees table (the widest) may follow it.
const report = makeReport(2023, 6);
const workbook = await render(report, [''], {'': [arrival()]});
const sheet = workbook.getWorksheet('Rechnungsempfänger');
for (let column = 1; column <= 12; column++) {
expect(sheet.getRow(1).getCell(column).fill).toEqual(GRAY_FILL);
}
expect(sheet.getRow(1).getCell(13).fill).toBeUndefined();
});

it('fills the subheader row and closes off its top edge with corner ticks', async () => {
const report = makeReport();
const workbook = await render(report, ['Company A'], {'Company A': [arrival()]});
const sheet = workbook.getWorksheet('Company A');
const subHeaderRow = sheet.getRow(3);

expect(subHeaderRow.getCell(1).border).toEqual({top: THIN_BLACK, left: THIN_BLACK});
for (let column = 2; column <= 11; column++) {
expect(subHeaderRow.getCell(column).border).toEqual({top: THIN_BLACK});
}
expect(subHeaderRow.getCell(12).border).toEqual({top: THIN_BLACK, right: THIN_BLACK});
for (let column = 1; column <= 12; column++) {
expect(subHeaderRow.getCell(column).fill).toEqual(GRAY_FILL);
}
});

it('fills and borders the header row across every column', async () => {
const report = makeReport();
const workbook = await render(report, ['Company A'], {'Company A': [arrival()]});
const sheet = workbook.getWorksheet('Company A');
const headerRow = sheet.getRow(4);
for (let column = 1; column <= 12; column++) {
expect(headerRow.getCell(column).fill).toEqual(GRAY_FILL);
expect(headerRow.getCell(column).border).toEqual({
top: THIN_BLACK, left: THIN_BLACK, bottom: THIN_BLACK, right: THIN_BLACK,
});
}
});

it('borders every data row cell without filling it', async () => {
const report = makeReport();
const workbook = await render(report, ['Company A'], {'Company A': [arrival()]});
const sheet = workbook.getWorksheet('Company A');
const dataRow = sheet.getRow(5);
for (let column = 1; column <= 12; column++) {
expect(dataRow.getCell(column).border).toEqual({
top: THIN_BLACK, left: THIN_BLACK, bottom: THIN_BLACK, right: THIN_BLACK,
});
expect(dataRow.getCell(column).fill).toEqual(NO_FILL);
}
});

it('fills and borders the totals row across every column, even non-total ones', async () => {
const report = makeReport();
const workbook = await render(report, ['Company A'], {'Company A': [arrival()]});
const sheet = workbook.getWorksheet('Company A');
const totalsRow = sheet.getRow(6);
for (let column = 1; column <= 12; column++) {
expect(totalsRow.getCell(column).fill).toEqual(GRAY_FILL);
expect(totalsRow.getCell(column).border).toEqual({
top: THIN_BLACK, left: THIN_BLACK, bottom: THIN_BLACK, right: THIN_BLACK,
});
}
});
});

describe('landing fees table', () => {
it('writes amounts as numbers, not formatted strings', async () => {
const report = makeReport();
Expand Down
33 changes: 32 additions & 1 deletion src/util/InvoicesExcelReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ export const XLSX_MIME_TYPE =
const MONEY_FORMAT = '#,##0.00';
const DATE_FORMAT = 'dd.mm.yyyy';

const HEADER_FILL = {type: 'pattern', pattern: 'solid', fgColor: {argb: 'FFD9D9D9'}} as const;
const THIN_BLACK = {style: 'thin', color: {argb: 'FF000000'}} as const;
const GRID_BORDER = {top: THIN_BLACK, left: THIN_BLACK, bottom: THIN_BLACK, right: THIN_BLACK};
// The line above the subheader row: a plain top edge, with a left/right tick
// at the row's own edges closing off the table's outline from above.
const subHeaderBorder = (index: number, lastIndex: number) => ({
top: THIN_BLACK,
...(index === 0 ? {left: THIN_BLACK} : {}),
...(index === lastIndex ? {right: THIN_BLACK} : {}),
});

// Excel rejects these characters in worksheet names and caps them at 31 chars.
const FORBIDDEN_SHEET_NAME_CHARS = /[[\]:*?/\\]/g;
const MAX_SHEET_NAME_LENGTH = 31;
Expand Down Expand Up @@ -158,6 +169,8 @@ class InvoicesExcelReport extends InvoicesReportData {

const monthLabel = this.getMonthLabel();
const usedSheetNames: string[] = [];
const titleFillColumns =
Math.max(this.landingFeeColumns().length, this.customsFeeColumns().length);

recipientNames.forEach(recipientName => {
const sheetName = sanitizeSheetName(recipientName, usedSheetNames);
Expand All @@ -167,6 +180,12 @@ class InvoicesExcelReport extends InvoicesReportData {

const titleRow = sheet.addRow([`${recipientName} (${monthLabel})`]);
titleRow.getCell(1).font = {bold: true, size: 14};
// The title text overflows past its own cell, so fill the same span the
// widest table uses — otherwise the overflow sits on a plain white
// background instead of the banner.
for (let column = 1; column <= titleFillColumns; column++) {
titleRow.getCell(column).fill = HEADER_FILL;
}

this.addLandingFeesTable(sheet, arrivalRecipients[recipientName]);
this.addCustomsFeesTable(sheet, customsRecipients[recipientName], false);
Expand Down Expand Up @@ -312,9 +331,17 @@ class InvoicesExcelReport extends InvoicesReportData {

const subHeaderRow = sheet.addRow([subHeader]);
subHeaderRow.getCell(1).font = {bold: true, size: 12};
columns.forEach((_, index) => {
subHeaderRow.getCell(index + 1).fill = HEADER_FILL;
subHeaderRow.getCell(index + 1).border = subHeaderBorder(index, columns.length - 1);
});

const headerRow = sheet.addRow(columns.map(column => column.header));
headerRow.font = {bold: true};
columns.forEach((_, index) => {
headerRow.getCell(index + 1).fill = HEADER_FILL;
headerRow.getCell(index + 1).border = GRID_BORDER;
});

const firstDataRowNumber = headerRow.number + 1;

Expand All @@ -324,6 +351,7 @@ class InvoicesExcelReport extends InvoicesReportData {
if (column.numFmt) {
row.getCell(index + 1).numFmt = column.numFmt;
}
row.getCell(index + 1).border = GRID_BORDER;
});
});

Expand All @@ -332,11 +360,14 @@ class InvoicesExcelReport extends InvoicesReportData {
const totalsRow = sheet.addRow([]);
let totalIndex = 0;
columns.forEach((column, index) => {
const cell = totalsRow.getCell(index + 1);
cell.fill = HEADER_FILL;
cell.border = GRID_BORDER;

if (!column.total) {
return;
}
const letter = columnLetter(index + 1);
const cell = totalsRow.getCell(index + 1);
// The cached result keeps the total readable by tools that do not
// evaluate formulas; without it they see an empty cell.
cell.value = {
Expand Down
Loading