add event report generation
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
jorming.chong 2026-01-08 09:03:40 +08:00
parent 9ecd725e04
commit effe22c4b5
4 changed files with 150 additions and 6 deletions

Binary file not shown.

View File

@ -20,6 +20,17 @@ interface DeviceData {
readings: Record<string, Record<string, string>>; readings: Record<string, Record<string, string>>;
} }
interface EventData {
building: string;
floor: string;
device_name: string;
event_time: string;
event_type: string;
temperature: string;
humidity: string;
battery: string;
}
interface ReportParams { interface ReportParams {
start_date?: string; start_date?: string;
end_date?: string; end_date?: string;
@ -65,7 +76,7 @@ export class ReadingReportController extends Controller {
// ---------------- Title ---------------- // ---------------- Title ----------------
worksheet.mergeCells('B1:G2'); worksheet.mergeCells('B1:G2');
const titleCell = worksheet.getCell('B1'); const titleCell = worksheet.getCell('B1');
titleCell.value = 'Event Report'; titleCell.value = 'Reading Report';
titleCell.alignment = { horizontal: 'center', vertical: 'middle' }; titleCell.alignment = { horizontal: 'center', vertical: 'middle' };
titleCell.font = { bold: true, size: 18 }; titleCell.font = { bold: true, size: 18 };
@ -169,10 +180,10 @@ export class ReadingReportController extends Controller {
col.width = max + 2; col.width = max + 2;
}); });
worksheet.autoFilter = { // worksheet.autoFilter = {
from: { row: headerRowIndex, column: 1 }, // from: { row: headerRowIndex, column: 1 },
to: { row: currentRow - 1, column: headers.length }, // to: { row: currentRow - 1, column: headers.length },
}; // };
// ---------------- Save file ---------------- // ---------------- Save file ----------------
const outputPath = path.resolve('event_report.xlsx'); const outputPath = path.resolve('event_report.xlsx');
@ -183,6 +194,106 @@ export class ReadingReportController extends Controller {
return createFileOutput(readStream, 'event_report.xlsx'); 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
},
},
}), }),
); );
} }

View File

@ -51,3 +51,30 @@ export const FullReportInput = z.object({
}); });
export type FullReportInput = z.infer<typeof FullReportInput>; export type FullReportInput = z.infer<typeof FullReportInput>;
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<typeof FullEventInput>;

View File

@ -1,5 +1,5 @@
import { A, buildContract } from '@jig-software/trest-core'; import { A, buildContract } from '@jig-software/trest-core';
import { FullReportInput } from '../constants'; import { FullReportInput, FullEventInput } from '../constants';
export const readingReportContract = buildContract((c) => export const readingReportContract = buildContract((c) =>
c c
@ -12,5 +12,11 @@ export const readingReportContract = buildContract((c) =>
.path('/reading-report') .path('/reading-report')
.prepare((p) => p.body.json(FullReportInput).annotate<[query: A]>()) .prepare((p) => p.body.json(FullReportInput).annotate<[query: A]>())
.parse((p) => p.success.file(200)), .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)),
}), }),
); );