diff --git a/event_report.xlsx b/event_report.xlsx index 3512d84..ef63cda 100644 Binary files a/event_report.xlsx and b/event_report.xlsx differ diff --git a/src/controllers/ReadingReportController.ts b/src/controllers/ReadingReportController.ts index e714825..84fd5e3 100644 --- a/src/controllers/ReadingReportController.ts +++ b/src/controllers/ReadingReportController.ts @@ -20,6 +20,17 @@ interface DeviceData { readings: Record>; } +interface EventData { + building: string; + floor: string; + device_name: string; + event_time: string; + event_type: string; + temperature: string; + humidity: string; + battery: string; +} + interface ReportParams { start_date?: string; end_date?: string; @@ -65,7 +76,7 @@ export class ReadingReportController extends Controller { // ---------------- Title ---------------- worksheet.mergeCells('B1:G2'); const titleCell = worksheet.getCell('B1'); - titleCell.value = 'Event Report'; + titleCell.value = 'Reading Report'; titleCell.alignment = { horizontal: 'center', vertical: 'middle' }; titleCell.font = { bold: true, size: 18 }; @@ -169,10 +180,10 @@ export class ReadingReportController extends Controller { col.width = max + 2; }); - worksheet.autoFilter = { - from: { row: headerRowIndex, column: 1 }, - to: { row: currentRow - 1, column: headers.length }, - }; + // worksheet.autoFilter = { + // from: { row: headerRowIndex, column: 1 }, + // to: { row: currentRow - 1, column: headers.length }, + // }; // ---------------- Save file ---------------- const outputPath = path.resolve('event_report.xlsx'); @@ -183,6 +194,106 @@ export class ReadingReportController extends Controller { return createFileOutput(readStream, 'event_report.xlsx'); }, }, + generateEventsReport: { + handler: async (FullEventSchema) => { + const { data, report_params } = FullEventSchema; + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Event Records'); + + // ---------------- Logos ---------------- + const jecLogoId = workbook.addImage({ + filename: path.resolve('asset/jec_logo.jpg'), + extension: 'jpeg', + }); + const fastLogoId = workbook.addImage({ + filename: path.resolve('asset/fast_logo.jpg'), + extension: 'jpeg', + }); + + worksheet.addImage(jecLogoId, { + tl: { col: 0, row: 0 }, + ext: { width: 120, height: 120 }, + }); + worksheet.addImage(fastLogoId, { + tl: { col: 7, row: 0 }, + ext: { width: 120, height: 60 }, + }); + + // ---------------- Title ---------------- + worksheet.mergeCells('B1:G2'); + const titleCell = worksheet.getCell('B1'); + titleCell.value = 'Event Report'; + titleCell.alignment = { horizontal: 'center', vertical: 'middle' }; + titleCell.font = { bold: true, size: 18 }; + + // ---------------- Fixed params ---------------- + worksheet.getCell(4, 2).value = 'Start Date:'; + worksheet.getCell(4, 3).value = report_params.start_date; + worksheet.getCell(5, 2).value = 'End Date:'; + worksheet.getCell(5, 3).value = report_params.end_date; + + // ---------------- Headers ---------------- + const headers = [ + 'Building', + 'Floor', + 'Device Name', + 'Event Time', + 'Event Type', + 'Temperature', + 'Humidity', + 'Battery', + ]; + + const headerRowIndex = 7; + const headerRow = worksheet.getRow(headerRowIndex); + headerRow.values = headers; + headerRow.eachCell((cell) => { + cell.font = { bold: true }; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFD9D9D9' }, + }; + }); + + // ---------------- Data ---------------- + let currentRow = headerRowIndex + 1; + + data.forEach((event) => { + const row = worksheet.getRow(currentRow); + row.getCell(1).value = event.building; + row.getCell(2).value = event.floor; + row.getCell(3).value = event.device_name; + row.getCell(4).value = event.event_time; + row.getCell(5).value = event.event_type; + row.getCell(6).value = event.temperature; + row.getCell(7).value = event.humidity; + row.getCell(8).value = event.battery; + + currentRow++; + }); + + // ---------------- Auto-width ---------------- + worksheet.columns.forEach((col) => { + let max = 10; + col.eachCell!({ includeEmpty: true }, (cell) => { + const len = String(cell.value ?? '').length; + max = Math.max(max, len); + }); + col.width = max + 2; + }); + + // ---------------- Save file ---------------- + const outputPath = path.resolve('event_report.xlsx'); + await workbook.xlsx.writeFile(outputPath); + console.log(`Excel report generated: ${outputPath}`); + + // ---------------- Return as stream ---------------- + const readStream = fs.createReadStream(outputPath); + return createFileOutput(readStream, 'event_report.xlsx'); // your helper + }, + }, }), ); } diff --git a/src/shared/constants/reading.ts b/src/shared/constants/reading.ts index 0af326f..71ddf00 100644 --- a/src/shared/constants/reading.ts +++ b/src/shared/constants/reading.ts @@ -51,3 +51,30 @@ export const FullReportInput = z.object({ }); export type FullReportInput = z.infer; + +export const FullEventInput = z.object({ + data: z.array( + z.object({ + building: z.string(), + floor: z.string(), + device_name: z.string(), + event_time: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: 'Invalid event_time', + }), + event_type: z.string(), + temperature: z.string(), + humidity: z.string(), + battery: z.string(), + }), + ), + report_params: z.object({ + start_date: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: 'Invalid start_date', + }), + end_date: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: 'Invalid end_date', + }), + }), +}); + +export type FullEventInput = z.infer; diff --git a/src/shared/contracts/readingReportContract.ts b/src/shared/contracts/readingReportContract.ts index 67a7166..683108b 100644 --- a/src/shared/contracts/readingReportContract.ts +++ b/src/shared/contracts/readingReportContract.ts @@ -1,5 +1,5 @@ import { A, buildContract } from '@jig-software/trest-core'; -import { FullReportInput } from '../constants'; +import { FullReportInput, FullEventInput } from '../constants'; export const readingReportContract = buildContract((c) => c @@ -12,5 +12,11 @@ export const readingReportContract = buildContract((c) => .path('/reading-report') .prepare((p) => p.body.json(FullReportInput).annotate<[query: A]>()) .parse((p) => p.success.file(200)), + generateEventsReport: (e) => + e + .method('POST') + .path('/event-report') + .prepare((p) => p.body.json(FullEventInput).annotate<[query: A]>()) + .parse((p) => p.success.file(200)), }), );