167 lines
4.3 KiB
TypeScript
167 lines
4.3 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'
|
|
| 'docWeight'
|
|
| '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: 'docWeight',
|
|
label: 'Berat DOC',
|
|
value: isPresent(payload.docWeight) ? payload.docWeight : null,
|
|
unit: 'gram',
|
|
},
|
|
{
|
|
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' ||
|
|
metric.key === 'docWeight'
|
|
) {
|
|
return formatNumber(metric.value, 1, 0, true);
|
|
}
|
|
return formatNumber(metric.value, 0, 0, true);
|
|
}
|