update kpi report to include summary for daily report and end cycle
This commit is contained in:
1 parent
6c21af745d
commit
4439e3b7b1
5 files changed
+454
-43
No files matched your search
@@ -0,0 +1,158 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user