Files
dashboard-cpsp/utils/buildInMemoryReport.ts
T

210 lines
6.9 KiB
TypeScript

import type {
ChickenCounting,
ChickenWeight,
Cycle,
Karung,
KPI,
ManualInput,
} from '../types/api.ts';
import type { DashboardReportPayload, DashboardReportType } from '../types/dashboardReport.ts';
import { cycleDayForDate } from './format.ts';
export type BuildInMemoryReportInput = {
cycle: Cycle;
kandangId: number;
kandangName: string;
area: string;
reportType: DashboardReportType;
countings: ChickenCounting[];
manuals: ManualInput[];
weights: ChickenWeight[];
karungs: Karung[];
kpis: KPI[];
generatedAt?: string;
/** Override the report's last day (0-based); defaults to current day / cycle end. */
upToDay?: number;
};
function accuracy(iot: number | null, manual: number | null): number | null {
if (iot == null || manual == null || manual === 0) return null;
return Math.round((1 - Math.abs(iot - manual) / Math.abs(manual)) * 1000) / 10;
}
/** Feed accuracy: both manual and IoT reading zero means a perfect match (100%);
* if only one side has a reading, the match is 0%. */
function feedAccuracy(iot: number | null, manual: number | null): number | null {
if (manual != null && iot != null) {
if (manual === 0 && iot === 0) return 100;
if (manual === 0 || iot === 0) return 0;
}
return accuracy(iot, manual);
}
export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardReportPayload {
const { cycle } = input;
// Day numbering stays 0-based (day 0 = start_date), but the report must never
// run past the cycle end date. lastDay = start_date → end_date in days, which
// is total_days - 1 for a contiguous cycle.
const lastDay = cycleDayForDate(cycle.start_date, cycle.end_date);
const endCycleUpTo = Math.min(cycle.total_days, lastDay);
const defaultUpTo =
input.reportType === 'end_cycle'
? endCycleUpTo
: Math.min(Math.max(cycle.current_day, 0), lastDay);
const upToDay =
input.upToDay != null && Number.isFinite(input.upToDay)
? Math.min(Math.max(Math.floor(input.upToDay), 0), lastDay)
: defaultUpTo;
const days = Array.from({ length: Math.max(upToDay, 0) + 1 }, (_, day) => day);
const countingByDay = new Map<number, ChickenCounting>();
for (const row of input.countings) {
countingByDay.set(cycleDayForDate(cycle.start_date, row.date), row);
}
const weightByDay = new Map<number, ChickenWeight>();
for (const row of input.weights) {
weightByDay.set(cycleDayForDate(cycle.start_date, row.date), row);
}
const karungByDay = new Map<number, Karung>();
for (const row of input.karungs) {
karungByDay.set(cycleDayForDate(cycle.start_date, row.date), row);
}
const kpiByAge = new Map<number, KPI>();
for (const row of input.kpis) {
kpiByAge.set(row.age, row);
}
const manualWeightByDay = new Map<number, number>();
const manualFeedInByDay = new Map<number, number>();
const manualFeedUseByDay = new Map<number, number>();
const manualFeedOutByDay = new Map<number, number>();
for (const row of input.manuals) {
const day = cycleDayForDate(cycle.start_date, row.date);
if (row.manual_weight > 0) manualWeightByDay.set(day, row.manual_weight);
manualFeedInByDay.set(day, row.feed_in_manual);
manualFeedUseByDay.set(day, row.feed_use_manual);
manualFeedOutByDay.set(day, row.feed_out_manual);
}
const latestManual = input.manuals[0] ?? null;
const counting = days.map((day) => {
const iot = countingByDay.get(day) ?? null;
const kpi = kpiByAge.get(day) ?? null;
const iotCount = iot?.total_count ?? null;
const after = kpi?.chicken_life ?? iotCount;
return {
date: addDate(cycle.start_date, day),
day,
countBefore: after == null ? null : after + (kpi?.mortality ?? iot?.mortality_count ?? 0),
mortality: kpi?.mortality ?? iot?.mortality_count ?? latestManual?.mortality_manual ?? null,
harvest: kpi?.harvest ?? null,
harvestWeight: kpi?.harvest_weight ?? null,
countAfter: after,
iotCount,
accuracy: accuracy(iotCount, after),
};
});
const weight = days.map((day) => {
const iot = weightByDay.get(day) ?? null;
const avgIot = iot?.average_weight ?? null;
const avgManual = manualWeightByDay.get(day) ?? null;
return {
date: addDate(cycle.start_date, day),
day,
avgManual,
avgIot,
uniformity: iot?.uniformity ?? null,
difference: avgIot != null && avgManual != null ? avgIot - avgManual : null,
accuracy: accuracy(avgIot, avgManual),
};
});
const feedIn = days.map((day) => {
const karung = karungByDay.get(day) ?? null;
const iotIn = karung?.in_today ?? null;
const manualIn = manualFeedInByDay.get(day) ?? null;
return {
date: addDate(cycle.start_date, day),
manualIn,
iotIn,
accuracy: feedAccuracy(iotIn, manualIn),
};
});
const feedUse = days.map((day) => {
const karung = karungByDay.get(day) ?? null;
const iotUse = karung?.feed_use_today ?? karung?.out_today ?? null;
const manualUse = manualFeedUseByDay.get(day) ?? null;
return {
date: addDate(cycle.start_date, day),
day,
manualUse,
iotUse,
accuracy: feedAccuracy(iotUse, manualUse),
};
});
const feedOut = days.map((day) => {
const karung = karungByDay.get(day) ?? null;
const iotOut = karung?.out_today ?? null;
const manualOut = manualFeedOutByDay.get(day) ?? null;
return {
date: addDate(cycle.start_date, day),
manualOut,
iotOut,
accuracy: feedAccuracy(iotOut, manualOut),
};
});
const fcr = days.map((day) => {
const kpi = kpiByAge.get(day) ?? null;
const weightRow = weightByDay.get(day) ?? null;
return {
day,
iotFcr: kpi?.fcr ?? null,
feedTotal: kpi?.feed_total ?? null,
iotWeight: weightRow?.average_weight ?? kpi?.iot_weight ?? null,
chickenLife: kpi?.chicken_life ?? null,
harvestWeightTotal: kpi?.harvest_weight_total ?? null,
};
});
const eef = days.map((day) => {
const kpi = kpiByAge.get(day) ?? null;
return {
day,
iotEef: kpi?.eef ?? null,
chickenLifePercentage: kpi?.chicken_life_percentage ?? null,
iotAverageHarvestBw: kpi?.average_harvest_weight ?? null,
iotAverageDayWeight: kpi?.iot_weight ?? null,
iotFcr: kpi?.fcr ?? null,
};
});
return {
cycleId: String(cycle.id),
cycleStart: cycle.start_date,
cycleEnd: cycle.end_date,
kandangId: input.kandangId,
kandangName: input.kandangName,
area: input.area,
upToDay,
generatedAt: input.generatedAt ?? new Date().toISOString(),
reportType: input.reportType,
counting,
weight,
feedIn,
feedUse,
feedOut,
fcr,
eef,
};
}
function addDate(start: string, day: number): string {
const date = new Date(`${start}T00:00:00`);
date.setDate(date.getDate() + day);
const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}