Files
dashboard-cpsp/utils/reportSummary.ts
T

159 lines
4.1 KiB
TypeScript

import type { DashboardReportPayload } from '../types/dashboardReport.ts';
import {
countingTotals,
feedInTotals,
feedOutTotals,
feedUseTotals,
} from './reportTotals.ts';
import { formatNumber, formatRatio } from './format.ts';
export type ReportSummaryMetricKey =
| 'umur'
| 'populasi'
| 'mortalitas'
| 'panen'
| 'beratPanen'
| 'karungMasuk'
| 'karungDituang'
| 'beratPakan'
| 'karungKeluar'
| 'beratAvg'
| 'uniformity'
| 'fcr'
| 'eef';
export interface ReportSummaryMetric {
key: ReportSummaryMetricKey;
label: string;
value: number | null;
unit: string;
}
export interface ReportSummary {
metrics: ReportSummaryMetric[];
}
const isPresent = (value: number | null | undefined): value is number =>
typeof value === 'number' && Number.isFinite(value);
/** kg per karung — same factor as Dashboard feed weight panel */
const FEED_SACK_KG = 50;
export function latestValidRatio(
rows: Array<{ iotFcr?: number | null; iotEef?: number | null }>,
key: 'iotFcr' | 'iotEef'
): number | null {
for (let i = rows.length - 1; i >= 0; i -= 1) {
const value = rows[i]?.[key];
if (typeof value === 'number' && Number.isFinite(value)) return value;
}
return null;
}
export function buildReportSummary(payload: DashboardReportPayload): ReportSummary {
const lastCounting = payload.counting[payload.counting.length - 1] ?? null;
const lastWeight = payload.weight[payload.weight.length - 1] ?? null;
const counting = countingTotals(payload.counting);
const feedIn = feedInTotals(payload.feedIn);
const feedUse = feedUseTotals(payload.feedUse);
const feedOut = feedOutTotals(payload.feedOut);
const beratAvg = lastWeight
? isPresent(lastWeight.avgIot)
? lastWeight.avgIot
: isPresent(lastWeight.avgManual)
? lastWeight.avgManual
: null
: null;
const metrics: ReportSummaryMetric[] = [
{ key: 'umur', label: 'Umur', value: payload.upToDay, unit: 'hari' },
{
key: 'populasi',
label: 'Populasi',
value: isPresent(lastCounting?.countAfter) ? lastCounting!.countAfter : null,
unit: 'ekor',
},
{
key: 'mortalitas',
label: 'Mortalitas',
value: counting?.mortality ?? null,
unit: 'ekor',
},
{
key: 'panen',
label: 'Panen',
value: counting?.harvest ?? null,
unit: 'ekor',
},
{
key: 'beratPanen',
label: 'Berat Panen',
// counting harvestWeight is stored in grams; surface kg for the summary card
value: isPresent(counting?.harvestWeight) ? counting!.harvestWeight / 1000 : null,
unit: 'kg',
},
{
key: 'karungMasuk',
label: 'Karung Masuk',
value: feedIn?.manualIn ?? null,
unit: 'karung',
},
{
key: 'karungDituang',
label: 'Karung Dituang',
value: feedUse?.manualUse ?? null,
unit: 'karung',
},
{
key: 'beratPakan',
label: 'Berat Pakan',
value: isPresent(feedUse?.manualUse) ? feedUse!.manualUse * FEED_SACK_KG : null,
unit: 'kg',
},
{
key: 'karungKeluar',
label: 'Karung Keluar',
value: feedOut?.manualOut ?? null,
unit: 'karung',
},
{ key: 'beratAvg', label: 'Berat AVG', value: beratAvg, unit: 'gram' },
{
key: 'uniformity',
label: 'Uniformity',
value: isPresent(lastWeight?.uniformity) ? lastWeight!.uniformity : null,
unit: '%',
},
{
key: 'fcr',
label: 'FCR',
value: latestValidRatio(payload.fcr, 'iotFcr'),
unit: '',
},
{
key: 'eef',
label: 'EEF',
value: latestValidRatio(payload.eef, 'iotEef'),
unit: '',
},
];
return { metrics };
}
export function formatSummaryValue(metric: ReportSummaryMetric): string {
if (!isPresent(metric.value)) return '—';
if (metric.key === 'fcr' || metric.key === 'eef') {
return formatRatio(metric.value, 3);
}
if (
metric.key === 'uniformity' ||
metric.key === 'beratAvg' ||
metric.key === 'beratPanen' ||
metric.key === 'beratPakan'
) {
return formatNumber(metric.value, 1, 0, true);
}
return formatNumber(metric.value, 0, 0, true);
}