first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,231 @@
|
||||
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;
|
||||
}
|
||||
|
||||
function countingAccuracy(iot: number | null, stockAkhir: number | null): number | null {
|
||||
if (iot == null || stockAkhir == null || stockAkhir <= 0) return null;
|
||||
return Math.round((Math.min(iot, stockAkhir) / Math.max(iot, stockAkhir)) * 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 manualByDay = new Map<number, ManualInput>();
|
||||
for (const row of input.manuals) {
|
||||
manualByDay.set(cycleDayForDate(cycle.start_date, row.date), 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);
|
||||
}
|
||||
let previousStockAkhir: number | null = null;
|
||||
const counting = days.map((day) => {
|
||||
const iot = countingByDay.get(day) ?? null;
|
||||
const kpi = kpiByAge.get(day) ?? null;
|
||||
const manual = manualByDay.get(day) ?? null;
|
||||
const iotCount = iot?.total_count ?? null;
|
||||
const mortality =
|
||||
manual?.mortality_manual ?? kpi?.mortality ?? iot?.mortality_count ?? null;
|
||||
const harvest = manual?.harvest_manual ?? kpi?.harvest ?? null;
|
||||
const harvestWeight = manual?.harvest_weight_manual ?? kpi?.harvest_weight ?? null;
|
||||
const mati = mortality ?? 0;
|
||||
const panen = harvest ?? 0;
|
||||
const countBefore = day === 0 ? cycle.doc_in_count || 0 : (previousStockAkhir ?? 0);
|
||||
const countAfter = kpi?.chicken_life ?? Math.max(0, countBefore - mati - panen);
|
||||
previousStockAkhir = countAfter;
|
||||
|
||||
return {
|
||||
date: addDate(cycle.start_date, day),
|
||||
day,
|
||||
countBefore,
|
||||
mortality,
|
||||
harvest,
|
||||
harvestWeight,
|
||||
countAfter,
|
||||
iotCount,
|
||||
accuracy: countingAccuracy(iotCount, countAfter),
|
||||
};
|
||||
});
|
||||
|
||||
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,
|
||||
docWeight:
|
||||
typeof cycle.chick_in_weight === 'number' && Number.isFinite(cycle.chick_in_weight)
|
||||
? cycle.chick_in_weight
|
||||
: null,
|
||||
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())}`;
|
||||
}
|
||||
Reference in new issue
Block a user