adjust the kpi card minigraph to be limited by publish cutoff time
This commit is contained in:
1 parent
3ba6fed189
commit
155a24c7bd
3 files changed
+73
-10
No files matched your search
+21
-10
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user