From 260960c43859c8537d3b16d9c925095cacfc3487 Mon Sep 17 00:00:00 2001 From: 1AhmedYasser <26207361+1AhmedYasser@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:36:20 +0200 Subject: [PATCH 1/3] chore(1797): Added download chat sheet enhancements --- controllers/conversion.test.ts | 44 +++++++------ controllers/conversion.ts | 117 +++++++++++++++++++++++++-------- 2 files changed, 113 insertions(+), 48 deletions(-) diff --git a/controllers/conversion.test.ts b/controllers/conversion.test.ts index 212bf1d..ae49bc1 100644 --- a/controllers/conversion.test.ts +++ b/controllers/conversion.test.ts @@ -710,14 +710,16 @@ 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); }); @@ -740,18 +742,20 @@ 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); }); diff --git a/controllers/conversion.ts b/controllers/conversion.ts index 6c5cb1c..0930b17 100644 --- a/controllers/conversion.ts +++ b/controllers/conversion.ts @@ -402,6 +402,20 @@ router.post( }, ); +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; @@ -409,18 +423,69 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('Chats'); 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}`); + 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), '', '']); + [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, '', '']); + [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, '']); + [1, 2, 3].forEach((col) => applyCellStyle(worksheet.getCell(row.number, col))); + }); + + const messagesSectionRow = worksheet.addRow([messagesSectionLabel, '', '']); + [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]); + [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, { @@ -435,28 +500,24 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: }) .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(); From 35d7a60f88eb6876b388b1bb36c34abbf179ce46 Mon Sep 17 00:00:00 2001 From: 1AhmedYasser <26207361+1AhmedYasser@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:52:43 +0200 Subject: [PATCH 2/3] chore(1797): Added Minimum Row Height --- controllers/conversion.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/controllers/conversion.ts b/controllers/conversion.ts index 0930b17..9cdbe9a 100644 --- a/controllers/conversion.ts +++ b/controllers/conversion.ts @@ -422,6 +422,10 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: 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) => + Math.max(MIN_ROW_HEIGHT, (Math.ceil(((text ?? '').length || 1) / colWidthChars) || 1) * POINTS_PER_LINE); const dateLocale = language === 'en' ? 'en-GB' : 'et-EE'; const createdLabel = language === 'en' ? 'Created' : 'Loodud'; const authorLabel = language === 'en' ? 'Author' : 'Autor'; @@ -439,6 +443,7 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: 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: { @@ -452,6 +457,7 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: }); const chatDataSectionRow = worksheet.addRow([chatDataSectionLabel, '', '']); + chatDataSectionRow.height = MIN_ROW_HEIGHT; [1, 2, 3].forEach((col) => { applyCellStyle(worksheet.getCell(chatDataSectionRow.number, col), { fill: { @@ -465,10 +471,12 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: 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: { @@ -480,6 +488,7 @@ router.post('/chats-to-xlsx', async (req: Request<{}, {}, ChatsToXlsxBody>, res: }); 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 From a4d8bbf8736c1dc1d42f37a79190c29a0cf6d9c2 Mon Sep 17 00:00:00 2001 From: 1AhmedYasser <26207361+1AhmedYasser@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:17:40 +0200 Subject: [PATCH 3/3] fix(1797): Fixed test --- controllers/conversion.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/controllers/conversion.test.ts b/controllers/conversion.test.ts index ae49bc1..2846061 100644 --- a/controllers/conversion.test.ts +++ b/controllers/conversion.test.ts @@ -719,7 +719,6 @@ describe('conversion controller', () => { ['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); }); @@ -748,14 +747,12 @@ describe('conversion controller', () => { ['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); }); @@ -768,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);