adjust the kpi card minigraph to be limited by publish cutoff time

This commit is contained in:
Alberto-Audrix committed 2026-09-07 14:30:55 +07:00
1 parent 3ba6fed189
commit 155a24c7bd
3 files changed
+73 -10

No files matched your search

+21 -10
View File
@@ -19,6 +19,7 @@ import {
formatNumber,
resolveCycleStatus,
} from '../utils/format.ts';
import { isOnOrBeforeVisibleDate, throughVisibleDate } from '../utils/visibility.ts';
import DashboardReportCtas from './dashboard/DashboardReportCtas.tsx';
import DashboardManualInputForm from './dashboard/DashboardManualInputForm.tsx';
import ReportPreviewOverlay from './dashboard/ReportPreviewOverlay.tsx';
@@ -96,26 +97,36 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
}, [selectedCycle]);
const cards: KpiCardModel[] = useMemo(() => {
const latest = kpis[0];
// Countings/weights APIs are real-time; KPI cards still honor publish cutoff.
const visibleThrough = selectedCycle?.visible_through_date;
const visibleKpis = throughVisibleDate(kpis, visibleThrough);
const visibleCountings = throughVisibleDate(countings, visibleThrough);
const visibleWeights = throughVisibleDate(weights, visibleThrough);
const visibleLatestAvg =
latestAvg && isOnOrBeforeVisibleDate(latestAvg.date, visibleThrough) ? latestAvg : null;
const visibleKarung =
karung && isOnOrBeforeVisibleDate(karung.date, visibleThrough) ? karung : null;
const latest = visibleKpis[0];
const startDate = selectedCycle?.start_date;
const trendLabel = (isoDate: string): string => {
const day = startDate ? cycleDayForDate(startDate, isoDate) : 0;
return `Hari ${day} · ${formatDateId(isoDate)}`;
};
const fcrTrend: KpiTrendPoint[] = chronological(kpis).map((row) => ({
const fcrTrend: KpiTrendPoint[] = chronological(visibleKpis).map((row) => ({
value: row.fcr,
label: trendLabel(row.date),
}));
const eefTrend: KpiTrendPoint[] = chronological(kpis).map((row) => ({
const eefTrend: KpiTrendPoint[] = chronological(visibleKpis).map((row) => ({
value: row.eef,
label: trendLabel(row.date),
}));
const mortalityTrend: KpiTrendPoint[] = chronological(countings).map((row) => ({
const mortalityTrend: KpiTrendPoint[] = chronological(visibleCountings).map((row) => ({
value: row.mortality_count,
label: trendLabel(row.date),
}));
const mortalityToday = latest?.mortality ?? countings[0]?.mortality_count ?? null;
const weightTrend: KpiTrendPoint[] = chronological(weights).map((row) => ({
const mortalityToday = latest?.mortality ?? visibleCountings[0]?.mortality_count ?? null;
const weightTrend: KpiTrendPoint[] = chronological(visibleWeights).map((row) => ({
value: row.average_weight,
label: trendLabel(row.date),
}));
@@ -142,20 +153,20 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
{
key: 'karung',
title: 'Karung Dituang',
value: karung?.feed_use_today ?? karung?.out_today ?? null,
value: visibleKarung?.feed_use_today ?? visibleKarung?.out_today ?? null,
unit: 'karung',
icon: '🛢️',
status: karung ? 'ok' : 'na',
status: visibleKarung ? 'ok' : 'na',
targetView: 'feedSackCounting',
},
{
key: 'weight',
title: 'Berat AVG',
value: latestAvg?.average_weight ?? weights[0]?.average_weight ?? null,
value: visibleLatestAvg?.average_weight ?? visibleWeights[0]?.average_weight ?? null,
unit: 'gram',
icon: '⚖️',
trend: weightTrend,
status: latestAvg || weights[0] ? 'ok' : 'na',
status: visibleLatestAvg || visibleWeights[0] ? 'ok' : 'na',
targetView: 'weightMonitoring',
},
{
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { isOnOrBeforeVisibleDate, throughVisibleDate } from '../visibility.ts';
describe('throughVisibleDate', () => {
const rows = [{ date: '2026-09-05' }, { date: '2026-09-06' }, { date: '2026-09-07' }];
it('drops rows after the publish cutoff', () => {
expect(throughVisibleDate(rows, '2026-09-06')).toEqual([
{ date: '2026-09-05' },
{ date: '2026-09-06' },
]);
});
it('returns all rows when cutoff is missing', () => {
expect(throughVisibleDate(rows, undefined)).toEqual(rows);
expect(throughVisibleDate(rows, null)).toEqual(rows);
});
});
describe('isOnOrBeforeVisibleDate', () => {
it('rejects dates after the cutoff', () => {
expect(isOnOrBeforeVisibleDate('2026-09-07', '2026-09-06')).toBe(false);
});
it('accepts dates on or before the cutoff', () => {
expect(isOnOrBeforeVisibleDate('2026-09-06', '2026-09-06')).toBe(true);
expect(isOnOrBeforeVisibleDate('2026-09-05', '2026-09-06')).toBe(true);
});
it('rejects empty dates', () => {
expect(isOnOrBeforeVisibleDate(null, '2026-09-06')).toBe(false);
expect(isOnOrBeforeVisibleDate(undefined, '2026-09-06')).toBe(false);
});
});
+18
View File
@@ -0,0 +1,18 @@
/** Keep rows on or before the dashboard publish cutoff date (ISO `YYYY-MM-DD`). */
export function throughVisibleDate<T extends { date: string }>(
rows: T[],
visibleThrough: string | null | undefined
): T[] {
if (!visibleThrough) return rows;
return rows.filter((row) => row.date <= visibleThrough);
}
/** True when `date` is on or before the publish cutoff (or cutoff is unknown). */
export function isOnOrBeforeVisibleDate(
date: string | null | undefined,
visibleThrough: string | null | undefined
): boolean {
if (!date) return false;
if (!visibleThrough) return true;
return date <= visibleThrough;
}