Add dashboard report snapshots feature with CRUD operations. Implemented new database migrations for dashboard_report_snapshots, including report type handling. Updated backend routes and models to support snapshot retrieval and upsert functionality. Enhanced frontend components to integrate report snapshot management, including UI for daily and end-cycle reports. Added tests for new functionality to ensure reliability.
CI / lint-and-test (push) Canceled after 0s
CI / lint-and-test (push) Canceled after 0s
This commit is contained in:
1 parent
059087be1d
commit
34336b9bd1
41 files changed
+4824
-178
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { DashboardReportPayload } from '../../types/dashboardReport.ts';
|
||||
import { buildReportWorkbook } from '../reportExcel.ts';
|
||||
|
||||
const snapshot: DashboardReportPayload = {
|
||||
cycleId: 'CYCLE-JBW',
|
||||
kandangId: 1,
|
||||
kandangName: 'Kandang Atas',
|
||||
area: 'Sukawarna',
|
||||
upToDay: 2,
|
||||
generatedAt: '2026-05-24T10:00:00.000Z',
|
||||
counting: [
|
||||
{
|
||||
date: '2026-05-22',
|
||||
day: 0,
|
||||
countBefore: 100,
|
||||
mortality: 0,
|
||||
harvest: 0,
|
||||
harvestWeight: null,
|
||||
countAfter: 100,
|
||||
iotCount: 95,
|
||||
accuracy: 95,
|
||||
},
|
||||
],
|
||||
weight: [
|
||||
{
|
||||
date: '2026-05-23',
|
||||
day: 1,
|
||||
avgManual: 1000,
|
||||
avgIot: 900,
|
||||
uniformity: 80,
|
||||
difference: 100,
|
||||
accuracy: 90,
|
||||
},
|
||||
],
|
||||
feedIn: [{ date: '2026-05-22', manualIn: 10, iotIn: 10, accuracy: 100 }],
|
||||
feedUse: [{ date: '2026-05-22', day: 0, manualUse: 4, iotUse: 4, accuracy: 100 }],
|
||||
};
|
||||
|
||||
const emptySnapshot: DashboardReportPayload = {
|
||||
...snapshot,
|
||||
counting: [],
|
||||
weight: [],
|
||||
feedIn: [],
|
||||
feedUse: [],
|
||||
};
|
||||
|
||||
describe('buildReportWorkbook', () => {
|
||||
it('creates four sheets with the specified names', async () => {
|
||||
const workbook = await buildReportWorkbook(snapshot);
|
||||
expect(workbook.worksheets.map((sheet) => sheet.name)).toEqual([
|
||||
'Counting',
|
||||
'Weight',
|
||||
'Feed In',
|
||||
'Feed Use',
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes chicken age and mortality on Counting, and no chicken-age on Feed In', async () => {
|
||||
const workbook = await buildReportWorkbook(snapshot);
|
||||
const countingHeaders = workbook.getWorksheet('Counting')?.getRow(2).values ?? [];
|
||||
const feedInHeaders = workbook.getWorksheet('Feed In')?.getRow(2).values ?? [];
|
||||
|
||||
expect(countingHeaders).toEqual(
|
||||
expect.arrayContaining(['Umur (hari)', 'Mortalitas'])
|
||||
);
|
||||
expect(feedInHeaders).toEqual(expect.arrayContaining(['Tanggal', 'Manual masuk', 'IoT masuk']));
|
||||
expect(feedInHeaders).not.toEqual(expect.arrayContaining(['Umur (hari)']));
|
||||
});
|
||||
|
||||
it('writes header metadata matching upToDay and generatedAt', async () => {
|
||||
const workbook = await buildReportWorkbook(snapshot);
|
||||
const headerValues = workbook.getWorksheet('Counting')?.getRow(1).values ?? [];
|
||||
const header = (Array.isArray(headerValues) ? headerValues : [])
|
||||
.map((value) => (value == null ? '' : String(value)))
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
expect(header).toContain('Kandang Atas');
|
||||
expect(header).toContain('CYCLE-JBW');
|
||||
expect(header).toContain('Hari ke-0 s/d ke-2');
|
||||
expect(header).toContain('2026-05-24T10:00:00.000Z');
|
||||
});
|
||||
|
||||
it('still produces a workbook when snapshot tables are empty (AE5)', async () => {
|
||||
const workbook = await buildReportWorkbook(emptySnapshot);
|
||||
expect(workbook.worksheets).toHaveLength(4);
|
||||
expect(workbook.getWorksheet('Counting')?.rowCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('appends a Total row on each sheet', async () => {
|
||||
const workbook = await buildReportWorkbook(snapshot);
|
||||
const lastValues = (sheetName: string) => {
|
||||
const sheet = workbook.getWorksheet(sheetName);
|
||||
const values = sheet?.getRow(sheet.rowCount).values;
|
||||
return (Array.isArray(values) ? values : []).filter((value) => value != null);
|
||||
};
|
||||
|
||||
expect(lastValues('Counting')[0]).toBe('Total');
|
||||
expect(lastValues('Weight')[0]).toBe('2026-05-23');
|
||||
expect(lastValues('Feed In')).toEqual(['Total', 10, 10, '100.0%']);
|
||||
expect(lastValues('Feed Use')).toEqual(['Total', '—', 4, 4, '100.0%']);
|
||||
});
|
||||
|
||||
it('adds FCR and EEF sheets for an end-cycle snapshot', async () => {
|
||||
const workbook = await buildReportWorkbook({
|
||||
...snapshot,
|
||||
reportType: 'end_cycle',
|
||||
fcr: [
|
||||
{
|
||||
day: 0,
|
||||
iotFcr: 1.234,
|
||||
feedTotal: 10,
|
||||
iotWeight: 45.5,
|
||||
chickenLife: 10000,
|
||||
harvestWeightTotal: 12000,
|
||||
},
|
||||
],
|
||||
eef: [
|
||||
{
|
||||
day: 0,
|
||||
iotEef: 310.456,
|
||||
chickenLifePercentage: 99.5,
|
||||
iotAverageHarvestBw: 45.5,
|
||||
iotAverageDayWeight: 0.2,
|
||||
iotFcr: 1.234,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(workbook.worksheets.map((sheet) => sheet.name)).toEqual([
|
||||
'Counting',
|
||||
'Weight',
|
||||
'Feed In',
|
||||
'Feed Use',
|
||||
'FCR',
|
||||
'EEF',
|
||||
]);
|
||||
|
||||
const headerCells = (sheetName: string) => {
|
||||
const values = workbook.getWorksheet(sheetName)?.getRow(2).values ?? [];
|
||||
return (Array.isArray(values) ? values : []).filter((value) => value != null);
|
||||
};
|
||||
|
||||
expect(headerCells('FCR')).toEqual([
|
||||
'Umur',
|
||||
'Total Pakan',
|
||||
'Bobot IoT',
|
||||
'Ayam Hidup',
|
||||
'Total Bobot Panen',
|
||||
'FCR',
|
||||
]);
|
||||
expect(headerCells('EEF')).toEqual([
|
||||
'Umur',
|
||||
'Persentase Ayam Hidup',
|
||||
'BW Rata-rata Panen',
|
||||
'Umur Rata-rata Panen',
|
||||
'FCR',
|
||||
'EEF',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user