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

This commit is contained in:
Alberto-Audrix committed 2026-08-13 15:46:16 +07:00
1 parent 059087be1d
commit 34336b9bd1
41 files changed
+4824 -178

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
import type { DashboardReportEefRow, DashboardReportFcrRow } from '../types/dashboardReport.ts';
type ScalesKpiRow = {
food_total?: number;
iot_weight?: number;
chicken_life?: number;
harvest_weight_total?: number;
iot_fcr?: number;
chicken_life_percentage?: number;
iot_average_harvest_bw?: number;
iot_average_day_weight?: number;
iot_eef?: number;
};
const toValidRatio = (value: number | undefined): number | null =>
value !== undefined && value > 0 ? value : null;
const toNullableNumber = (value: number | undefined | null): number | null =>
value !== undefined && value !== null ? value : null;
const isKpiMap = (value: unknown): value is Record<string, ScalesKpiRow> =>
!!value && typeof value === 'object' && !Array.isArray(value);
const kpiDaysInRange = (
kpi: unknown,
upToDay: number
): Array<[number, ScalesKpiRow]> => {
if (!isKpiMap(kpi)) return [];
return Object.entries(kpi)
.map(([dayKey, row]) => [Number(dayKey), row || {}] as [number, ScalesKpiRow])
.filter(([day]) => Number.isFinite(day) && day >= 0 && day <= upToDay)
.sort((a, b) => a[0] - b[0]);
};
export function mapKpiToFcrRows(kpi: unknown, upToDay: number): DashboardReportFcrRow[] {
return kpiDaysInRange(kpi, upToDay).map(([day, row]) => ({
day,
feedTotal: row.food_total ?? 0,
iotWeight: toNullableNumber(row.iot_weight),
chickenLife: row.chicken_life ?? 0,
harvestWeightTotal: row.harvest_weight_total ?? 0,
iotFcr: toValidRatio(row.iot_fcr),
}));
}
export function mapKpiToEefRows(kpi: unknown, upToDay: number): DashboardReportEefRow[] {
return kpiDaysInRange(kpi, upToDay).map(([day, row]) => ({
day,
chickenLifePercentage: toNullableNumber(row.chicken_life_percentage),
iotAverageHarvestBw: toNullableNumber(row.iot_average_harvest_bw),
iotAverageDayWeight: toNullableNumber(row.iot_average_day_weight),
iotFcr: toValidRatio(row.iot_fcr),
iotEef: toValidRatio(row.iot_eef),
}));
}
export function latestValidRatio<T>(
rows: T[] | null | undefined,
key: keyof T
): number | null {
if (!rows?.length) return null;
for (let i = rows.length - 1; i >= 0; i--) {
const value = rows[i]?.[key];
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
return value;
}
}
return null;
}