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
53 changes: 31 additions & 22 deletions controllers/conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,14 +710,15 @@ describe('conversion controller', () => {
.send({ file: { 'test.xlsx': res.body.base64String } });

expect(xlsxDataArray.body).toEqual([
['Vestlus #1'],
['Header1', 'Header2'],
['Row1', 'Row2'],
['Sõnumid'],
['', 'Loodud', 'Bot', 'Client', 'CSA'],
['', '01.01.2024 12:00:00', 'Buerokratt message', '', ''],
['', '01.01.2024 12:01:00', '', 'End-user message', ''],
['', '01.01.2024 12:02:00', '', '', 'Other message'],
['Vestlus #1', '', ''],
['Vestluse andmed', '', ''],
['Header1', 'Row1', ''],
['Header2', 'Row2', ''],
['Sõnumid', '', ''],
['Loodud', 'Autor', 'Sõnum'],
['01.01.2024 12:00:00', 'Bürokratt', 'Buerokratt message'],
['01.01.2024 12:01:00', 'Lõppkasutaja', 'End-user message'],
['01.01.2024 12:02:00', 'CSA', 'Other message'],
]);
expect(res.status).toBe(200);
});
Expand All @@ -740,18 +741,18 @@ describe('conversion controller', () => {
.send({ file: { 'test.xlsx': res.body.base64String } });

expect(xlsxDataArray.body).toEqual([
['Chat #1'],
['Header'],
['Row1'],
['Messages'],
['', 'Created', 'Bot', 'Client', 'CSA'],
['', '01.01.2024 12:00:00', 'Chat 1 message', '', ''],
['Chat #2'],
['Header'],
['Row2'],
['Messages'],
['', 'Created', 'Bot', 'Client', 'CSA'],
['', '01.01.2024 15:00:00', '', 'Chat 2 message', ''],
['Chat #1', '', ''],
['Chat data', '', ''],
['Header', 'Row1', ''],
['Messages', '', ''],
['Created', 'Author', 'Message'],
['01.01.2024 12:00:00', 'Bürokratt', 'Chat 1 message'],
['Chat #2', '', ''],
['Chat data', '', ''],
['Header', 'Row2', ''],
['Messages', '', ''],
['Created', 'Author', 'Message'],
['01.01.2024 15:00:00', 'End-user', 'Chat 2 message'],
]);
expect(res.status).toBe(200);
});
Expand All @@ -764,13 +765,21 @@ describe('conversion controller', () => {
chatIds: ['1'],
};

let rowNumber = 0;
const mockColumn = { width: 0 };
const mockCell = { alignment: {}, fill: undefined, font: {} };
const mockWorksheet = {
addRow: vi.fn(),
columns: [null, { eachCell: 'function' }, { eachCell: (): void => {}, width: 0 }],
addRow: vi.fn().mockImplementation(() => ({ number: ++rowNumber, height: undefined })),
getColumn: vi.fn().mockReturnValue(mockColumn),
getCell: vi.fn().mockReturnValue(mockCell),
};
vi.spyOn(ExcelJS.Workbook.prototype, 'addWorksheet').mockReturnValueOnce(
mockWorksheet as unknown as ExcelJS.Worksheet,
);
const mockBuffer = Buffer.from('mock-xlsx');
vi.spyOn(ExcelJS.Workbook.prototype, 'xlsx', 'get').mockReturnValueOnce({
writeBuffer: vi.fn().mockResolvedValue(mockBuffer),
} as unknown as ExcelJS.Xlsx);

const res = await request(app).post('/conversion/chats-to-xlsx').send(data);

Expand Down
126 changes: 98 additions & 28 deletions controllers/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,25 +402,99 @@
},
);

function applyCellStyle(
cell: ExcelJS.Cell,
opts: { fill?: ExcelJS.Fill; bold?: boolean; font?: { color?: { argb: string } } } = {},
): void {
cell.alignment = {
wrapText: true,
vertical: 'bottom' as const,
horizontal: 'left' as const,
};
if (opts.fill) cell.fill = opts.fill;
if (opts.bold) cell.font = { ...cell.font, bold: true };
if (opts.font?.color) cell.font = { ...cell.font, color: opts.font.color };
}

router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: Response) => {
try {
const { chatMessages, chatHeaders, chatRows, chatIds, language = 'et' } = req.body;

const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Chats');
const MIN_ROW_HEIGHT = 16;
const POINTS_PER_LINE = 14;
const minHeightForWrappedText = (text: string, colWidthChars: number) =>

Check failure on line 427 in controllers/conversion.ts

View workflow job for this annotation

GitHub Actions / lint / check

Missing return type on function

Check failure on line 427 in controllers/conversion.ts

View workflow job for this annotation

GitHub Actions / lint / check

Missing return type on function
Math.max(MIN_ROW_HEIGHT, (Math.ceil(((text ?? '').length || 1) / colWidthChars) || 1) * POINTS_PER_LINE);
const dateLocale = language === 'en' ? 'en-GB' : 'et-EE';
const messagesHeaderLabel = language === 'en' ? 'Messages' : 'Sõnumid';
const createdLabel = language === 'en' ? 'Created' : 'Loodud';
const authorLabel = language === 'en' ? 'Author' : 'Autor';
const messageLabel = language === 'en' ? 'Message' : 'Sõnum';
const chatNumberLabel = (n: number) => (language === 'en' ? `Chat #${n}` : `Vestlus #${n}`);

Check failure on line 433 in controllers/conversion.ts

View workflow job for this annotation

GitHub Actions / lint / check

Missing return type on function

Check failure on line 433 in controllers/conversion.ts

View workflow job for this annotation

GitHub Actions / lint / check

Missing return type on function
const chatDataSectionLabel = language === 'en' ? 'Chat data' : 'Vestluse andmed';
const messagesSectionLabel = language === 'en' ? 'Messages' : 'Sõnumid';

worksheet.getColumn(1).width = 20;
worksheet.getColumn(2).width = 30;
worksheet.getColumn(3).width = 100;

chatIds.forEach((chatId: string, index: number) => {
worksheet.addRow([language === 'en' ? `Chat #${index + 1}` : `Vestlus #${index + 1}`]);
worksheet.addRow(chatHeaders);
worksheet.addRow(chatRows[index]);
worksheet.addRow([messagesHeaderLabel]);
const headerRow = ['', language === 'en' ? 'Created' : 'Loodud', 'Bot', 'Client', 'CSA'];
worksheet.addRow(headerRow);
const chatRowValues = chatRows[index] ?? [];
const chatNumber = index + 1;

const startRow = worksheet.addRow([chatNumberLabel(chatNumber), '', '']);
startRow.height = MIN_ROW_HEIGHT;
[1, 2, 3].forEach((col) => {
applyCellStyle(worksheet.getCell(startRow.number, col), {
fill: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: '2F6EBA' },
},
bold: true,
font: { color: { argb: 'FFFFFFFF' } },
});
});

const chatDataSectionRow = worksheet.addRow([chatDataSectionLabel, '', '']);
chatDataSectionRow.height = MIN_ROW_HEIGHT;
[1, 2, 3].forEach((col) => {
applyCellStyle(worksheet.getCell(chatDataSectionRow.number, col), {
fill: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'D9D9D9' },
},
});
});

chatHeaders.forEach((key, i) => {
const value = chatRowValues[i] ?? '';
const row = worksheet.addRow([key, value, '']);
row.height = minHeightForWrappedText(String(value), 30);
[1, 2, 3].forEach((col) => applyCellStyle(worksheet.getCell(row.number, col)));
});

const messagesSectionRow = worksheet.addRow([messagesSectionLabel, '', '']);
messagesSectionRow.height = MIN_ROW_HEIGHT;
[1, 2, 3].forEach((col) => {
applyCellStyle(worksheet.getCell(messagesSectionRow.number, col), {
fill: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'D9D9D9' },
},
});
});

const messagesHeaderRow = worksheet.addRow([createdLabel, authorLabel, messageLabel]);
messagesHeaderRow.height = MIN_ROW_HEIGHT;
[1, 2, 3].forEach((col) => applyCellStyle(worksheet.getCell(messagesHeaderRow.number, col)));

const relatedMessages = chatMessages
.filter((msg: ChatMessage) => msg.chatId === chatId)
.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());

relatedMessages.forEach((msg: ChatMessage) => {
const formattedDateTime = new Date(msg.created)
.toLocaleString(dateLocale, {
Expand All @@ -435,28 +509,24 @@
})
.replaceAll('/', '.')
.replaceAll(',', '');
const row = ['', formattedDateTime, '', '', ''];
if (msg.authorRole === 'buerokratt') {
row[2] = msg.content;
} else if (msg.authorRole === 'end-user') {
row[3] = msg.content;
} else {
row[4] = msg.content;
}
worksheet.addRow(row);
});
worksheet.addRow([]);
});

worksheet.columns.forEach((col) => {
if (!col || typeof col.eachCell !== 'function') return;

let maxLength = 10;
col.eachCell({ includeEmpty: true }, (cell) => {
const length = cell.value ? cell.value.toString().length : 0;
if (length > maxLength) maxLength = length;
let author: string;
if (msg.authorRole === 'buerokratt') author = 'Bürokratt';
else if (msg.authorRole === 'end-user') author = language === 'en' ? 'End-user' : 'Lõppkasutaja';
else author = 'CSA';
const row = worksheet.addRow([formattedDateTime, author, msg.content]);
const isEndUser = msg.authorRole === 'end-user';
[1, 2, 3].forEach((col) => {
applyCellStyle(worksheet.getCell(row.number, col), {
fill: isEndUser
? undefined
: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFDDEBF7' },
},
});
});
});
col.width = maxLength + 2;
});

const buffer = await workbook.xlsx.writeBuffer();
Expand Down
Loading