fix bug fitur kalender berat dan cap panel iot
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-29 15:02:58 +07:00
1 parent b6117ae7d5
commit 4726437f38
5 files changed
+346 -63

No files matched your search

+15 -5
View File
@@ -1,5 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useAppContext } from '../context/AppContext.tsx';
import { getEstimasiPanenEndMs } from '../utils/dateFilter.ts';
import { baseCard } from './iotPanel/constants.tsx';
import { computeDashboard } from './iotPanel/computeDashboard.ts';
@@ -25,6 +28,13 @@ import type {
} from './iotPanel/types.ts';
const IotPanelPage: React.FC = () => {
const { cycleData } = useAppContext();
const iotMaxAtMs = useMemo(
() => getEstimasiPanenEndMs(cycleData),
[cycleData.endDate, cycleData.startDate, cycleData.totalDays]
);
const [data, setData] = useState<PayloadData | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -53,7 +63,7 @@ const IotPanelPage: React.FC = () => {
setIsRefreshing(true);
try {
const { raw, data: next } = await loadDashboardData();
const { raw, data: next } = await loadDashboardData({ maxAtMs: iotMaxAtMs });
setLastFleetRaw(raw);
@@ -71,7 +81,7 @@ const IotPanelPage: React.FC = () => {
} finally {
setIsRefreshing(false);
}
}, []);
}, [iotMaxAtMs]);
useEffect(() => {
void refreshDashboardData();
@@ -123,9 +133,9 @@ const IotPanelPage: React.FC = () => {
const flockId = row?.flock_id || row?.flock || null;
if (isDeviceHistoryMetric(metric)) {
setDeviceHistoryPoints(await loadDeviceHistory24h(metric, flockId));
setDeviceHistoryPoints(await loadDeviceHistory24h(metric, flockId, iotMaxAtMs));
} else {
setHistoryPoints(await loadIotSensorHistory24h(metric, flockId));
setHistoryPoints(await loadIotSensorHistory24h(metric, flockId, iotMaxAtMs));
}
} catch (e) {
setHistoryError(e instanceof Error ? e.message : 'Gagal memuat riwayat');
@@ -137,7 +147,7 @@ const IotPanelPage: React.FC = () => {
setHistoryLoading(false);
}
},
[lastFleetRaw]
[lastFleetRaw, iotMaxAtMs]
);
const closeSensorHistory = useCallback(() => {
+137 -13
View File
@@ -7,6 +7,117 @@ import {
import { normalizeTargetCelsius, resolveFan1DisplayTargetCelsius } from './fan1SetpointResolve.ts';
import type { IotFlockResultRow, IotFlocksRaw, PayloadData } from './types.ts';
type FlocksPageJson = IotFlocksRaw & { next?: string | null };
function rowUpdatedAtMs(row: IotFlockResultRow | null | undefined): number | null {
const rawAt = row?.payload?.data?.updatedAt;
if (!rawAt) return null;
const atMs = new Date(rawAt).getTime();
return Number.isFinite(atMs) ? atMs : null;
}
function compareRowIdAsc(a: IotFlockResultRow, b: IotFlockResultRow): number {
const aId = Number(a?.id);
const bId = Number(b?.id);
if (!Number.isFinite(aId) && !Number.isFinite(bId)) return 0;
if (!Number.isFinite(aId)) return -1;
if (!Number.isFinite(bId)) return 1;
return aId - bId;
}
function pickLatestRowById(rows: IotFlockResultRow[]): IotFlockResultRow | null {
if (rows.length === 0) return null;
const sortedById = [...rows].sort(compareRowIdAsc);
return sortedById[sortedById.length - 1] ?? null;
}
function pickLatestRowAtOrBefore(
rows: IotFlockResultRow[],
maxAtMs: number
): IotFlockResultRow | null {
const eligible = rows.filter((row) => {
const atMs = rowUpdatedAtMs(row);
return atMs !== null && atMs <= maxAtMs;
});
return pickLatestRowById(eligible);
}
async function fetchFlocksPage(page: number): Promise<FlocksPageJson> {
const response = await fetch(`${IOT_FLOCKS_API_URL}?page=${page}&_=${Date.now()}`, {
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Gagal memuat data IoT flocks API');
return (await response.json()) as FlocksPageJson;
}
/**
* Newest flock snapshot whose `updatedAt` is on/before `maxAtMs`.
* Pages are newest-first; binary-searches when the first page is entirely after the cap.
*/
async function findLatestFlockRowAtOrBefore(maxAtMs: number): Promise<{
raw: IotFlocksRaw;
row: IotFlockResultRow;
}> {
const firstPage = await fetchFlocksPage(1);
const firstRows = Array.isArray(firstPage.results) ? firstPage.results : [];
if (firstRows.length === 0) {
throw new Error('Data flock tidak ditemukan dari API');
}
const fromFirstPage = pickLatestRowAtOrBefore(firstRows, maxAtMs);
if (fromFirstPage) {
return {
raw: {
count: Number(fromFirstPage.id),
results: firstRows,
},
row: fromFirstPage,
};
}
const totalCount = Number(firstPage.count);
const pageSize = firstRows.length;
const totalPages =
Number.isFinite(totalCount) && totalCount > 0 && pageSize > 0
? Math.ceil(totalCount / pageSize)
: 1;
let lo = 2;
let hi = Math.max(1, totalPages);
let bestRow: IotFlockResultRow | null = null;
let bestPageRows: IotFlockResultRow[] = [];
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const page = await fetchFlocksPage(mid);
const rows = Array.isArray(page.results) ? page.results : [];
const eligible = pickLatestRowAtOrBefore(rows, maxAtMs);
if (eligible) {
bestRow = eligible;
bestPageRows = rows;
// Look for a newer eligible row on an earlier (newer) page.
hi = mid - 1;
} else {
// Entire page is after the cap — go older.
lo = mid + 1;
}
}
if (!bestRow) {
throw new Error('Tidak ada data IoT pada/ sebelum tanggal estimasi panen');
}
return {
raw: {
count: Number(bestRow.id),
results: bestPageRows.length > 0 ? bestPageRows : [bestRow],
},
row: bestRow,
};
}
export function latestResultRow(rawData: IotFlocksRaw | null): IotFlockResultRow | null {
const list = Array.isArray(rawData?.results) ? rawData.results : [];
if (list.length === 0) return null;
@@ -17,16 +128,7 @@ export function latestResultRow(rawData: IotFlocksRaw | null): IotFlockResultRow
if (byCountId) return byCountId;
}
const sortedById = [...list].sort((a, b) => {
const aId = Number(a?.id);
const bId = Number(b?.id);
if (!Number.isFinite(aId) && !Number.isFinite(bId)) return 0;
if (!Number.isFinite(aId)) return -1;
if (!Number.isFinite(bId)) return 1;
return aId - bId;
});
return sortedById[sortedById.length - 1] ?? null;
return pickLatestRowById(list);
}
export function apiResponseCounts(rawData: IotFlocksRaw | null): number {
@@ -285,14 +387,36 @@ async function loadTabelAlarmByFlockId(flockId: string): Promise<TabelAlarmPaylo
}
}
export async function loadDashboardData(): Promise<{ raw: IotFlocksRaw; data: PayloadData }> {
export type LoadDashboardDataOptions = {
/** Inclusive upper bound (ms). When set and live data is newer, freeze at last reading on/before this time. */
maxAtMs?: number | null;
};
export async function loadDashboardData(
options?: LoadDashboardDataOptions
): Promise<{ raw: IotFlocksRaw; data: PayloadData }> {
const maxAtMs =
options?.maxAtMs !== null && options?.maxAtMs !== undefined && Number.isFinite(options.maxAtMs)
? Number(options.maxAtMs)
: null;
let raw: IotFlocksRaw;
let latestRow: IotFlockResultRow | null;
if (maxAtMs !== null && Date.now() > maxAtMs) {
const capped = await findLatestFlockRowAtOrBefore(maxAtMs);
raw = capped.raw;
latestRow = capped.row;
} else {
const response = await fetch(`${IOT_FLOCKS_API_URL}?_=${Date.now()}`, {
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('Gagal memuat data IoT flocks API');
const raw = (await response.json()) as IotFlocksRaw;
const latestRow = latestResultRow(raw);
raw = (await response.json()) as IotFlocksRaw;
latestRow = latestResultRow(raw);
}
const result = latestRow?.payload?.data;
const latestId = latestRow?.id;
+152 -36
View File
@@ -119,34 +119,129 @@ function mergeDeviceHistoryEvents(events: TimedEvent[]): IotDeviceHistoryPoint[]
return merged;
}
async function loadFlockSnapshots24h(): Promise<FlockSnapshot[]> {
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
function historyWindowBounds(maxAtMs?: number | null): { upperMs: number; cutoffMs: number } {
const now = Date.now();
const upperMs =
maxAtMs !== null && maxAtMs !== undefined && Number.isFinite(maxAtMs)
? Math.min(now, Number(maxAtMs))
: now;
return { upperMs, cutoffMs: upperMs - 24 * 60 * 60 * 1000 };
}
function rowUpdatedAtMs(row: IotFlockResultRow | null | undefined): number | null {
const rawAt = row?.payload?.data?.updatedAt;
if (!rawAt) return null;
const atMs = new Date(rawAt).getTime();
return Number.isFinite(atMs) ? atMs : null;
}
async function fetchFlocksPage(page: number): Promise<IotFlocksRaw & { next?: string | null }> {
const response = await fetch(`${IOT_FLOCKS_API_URL}?page=${page}&_=${Date.now()}`, {
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error(`Gagal memuat riwayat (${response.status})`);
return (await response.json()) as IotFlocksRaw & { next?: string | null };
}
/**
* When the history window ends in the past (e.g. Estimasi Panen), jump to the newest
* page that still has rows on/before upperMs so we don't walk dozens of post-harvest pages.
*/
async function resolveFlocksHistoryStart(
upperMs: number
): Promise<{ startUrl: string; firstPage: (IotFlocksRaw & { next?: string | null }) | null }> {
if (Date.now() <= upperMs) {
return { startUrl: `${IOT_FLOCKS_API_URL}?_=${Date.now()}`, firstPage: null };
}
const firstPage = await fetchFlocksPage(1);
const firstRows = Array.isArray(firstPage.results) ? firstPage.results : [];
const hasEligibleOnFirst = firstRows.some((row) => {
const atMs = rowUpdatedAtMs(row);
return atMs !== null && atMs <= upperMs;
});
if (hasEligibleOnFirst) {
return { startUrl: `${IOT_FLOCKS_API_URL}?page=1&_=${Date.now()}`, firstPage };
}
const totalCount = Number(firstPage.count);
const pageSize = firstRows.length;
const totalPages =
Number.isFinite(totalCount) && totalCount > 0 && pageSize > 0
? Math.ceil(totalCount / pageSize)
: 1;
let lo = 2;
let hi = Math.max(1, totalPages);
let bestPage = totalPages;
let bestJson: (IotFlocksRaw & { next?: string | null }) | null = null;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const pageJson = await fetchFlocksPage(mid);
const rows = Array.isArray(pageJson.results) ? pageJson.results : [];
const hasEligible = rows.some((row) => {
const atMs = rowUpdatedAtMs(row);
return atMs !== null && atMs <= upperMs;
});
if (hasEligible) {
bestPage = mid;
bestJson = pageJson;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return {
startUrl: `${IOT_FLOCKS_API_URL}?page=${bestPage}&_=${Date.now()}`,
firstPage: bestJson,
};
}
async function loadFlockSnapshots24h(maxAtMs?: number | null): Promise<FlockSnapshot[]> {
const { upperMs, cutoffMs } = historyWindowBounds(maxAtMs);
const snapshots: FlockSnapshot[] = [];
let nextUrl: string | null = `${IOT_FLOCKS_API_URL}?_=${Date.now()}`;
const { startUrl, firstPage } = await resolveFlocksHistoryStart(upperMs);
let nextUrl: string | null = startUrl;
let pendingFirstPage = firstPage;
let safetyPageCount = 0;
const maxPages = 20;
const maxPages = 30;
while (nextUrl && safetyPageCount < maxPages) {
let json: IotFlocksRaw & { next?: string | null };
if (pendingFirstPage) {
json = pendingFirstPage;
pendingFirstPage = null;
} else {
const response = await fetch(nextUrl, {
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error(`Gagal memuat riwayat kipas (${response.status})`);
json = (await response.json()) as IotFlocksRaw & { next?: string | null };
}
const json = (await response.json()) as IotFlocksRaw & { next?: string | null };
const rows = Array.isArray(json.results) ? json.results : [];
if (rows.length === 0) break;
let pageHasNewerThanCutoff = false;
let pageHasDataInOrAfterWindow = false;
for (const row of rows) {
const payloadData = (row as IotFlockResultRow)?.payload?.data;
if (!payloadData?.updatedAt) continue;
const atMs = new Date(payloadData.updatedAt).getTime();
if (!Number.isFinite(atMs) || atMs < cutoffMs) continue;
if (!Number.isFinite(atMs)) continue;
if (atMs > upperMs) {
pageHasDataInOrAfterWindow = true;
continue;
}
if (atMs < cutoffMs) continue;
pageHasNewerThanCutoff = true;
pageHasDataInOrAfterWindow = true;
const normalized = preprocessFlockPayloadForPanel(payloadData);
const deviceStatus: Record<string, 0 | 1> = {};
for (let fanNumber = 1; fanNumber <= 11; fanNumber += 1) {
@@ -165,7 +260,7 @@ async function loadFlockSnapshots24h(): Promise<FlockSnapshot[]> {
});
}
if (!pageHasNewerThanCutoff) break;
if (!pageHasDataInOrAfterWindow) break;
const nextFromApi = json.next ?? null;
nextUrl = nextFromApi ? nextFromApi.replace('http://', 'https://') : null;
@@ -187,12 +282,13 @@ function statusEventsFromSnapshots(snapshots: FlockSnapshot[], deviceKey: string
async function loadFanSetpointLogEvents24h(
flockId: string,
fanNumber: number,
flockSnapshots: FlockSnapshot[]
flockSnapshots: FlockSnapshot[],
maxAtMs?: number | null
): Promise<TimedEvent[]> {
const safeId = String(flockId || '').trim();
if (!safeId) return [];
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
const { upperMs, cutoffMs } = historyWindowBounds(maxAtMs);
const base = `${IOT_DASHBOARD_IOT_API_BASE}/flock/${encodeURIComponent(safeId)}/setpoint/logs/`;
const events: TimedEvent[] = [];
@@ -216,7 +312,7 @@ async function loadFanSetpointLogEvents24h(
const rawAt = row.fetched_at || row.updated_at;
if (!rawAt) continue;
const atMs = new Date(rawAt).getTime();
if (!Number.isFinite(atMs) || atMs < cutoffMs) continue;
if (!Number.isFinite(atMs) || atMs < cutoffMs || atMs > upperMs) continue;
const inner = row.payload?.data;
const nearest = nearestFlockSnapshot(flockSnapshots, atMs);
@@ -233,12 +329,13 @@ async function loadFanSetpointLogEvents24h(
async function loadHvacSetpointLogEvents24h(
flockId: string,
deviceKey: HvacDeviceKey,
flockSnapshots: FlockSnapshot[]
flockSnapshots: FlockSnapshot[],
maxAtMs?: number | null
): Promise<TimedEvent[]> {
const safeId = String(flockId || '').trim();
if (!safeId) return [];
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
const { upperMs, cutoffMs } = historyWindowBounds(maxAtMs);
const base = `${IOT_DASHBOARD_IOT_API_BASE}/flock/${encodeURIComponent(safeId)}/setpoint/logs/`;
const events: TimedEvent[] = [];
@@ -262,7 +359,7 @@ async function loadHvacSetpointLogEvents24h(
const rawAt = row.fetched_at || row.updated_at;
if (!rawAt) continue;
const atMs = new Date(rawAt).getTime();
if (!Number.isFinite(atMs) || atMs < cutoffMs) continue;
if (!Number.isFinite(atMs) || atMs < cutoffMs || atMs > upperMs) continue;
const inner = row.payload?.data;
const nearest = nearestFlockSnapshot(flockSnapshots, atMs);
@@ -281,19 +378,21 @@ async function loadHvacSetpointLogEvents24h(
/**
* 24h device chart: setpoint (°C, from setpoint logs + panel logic) and status (flocks snapshots).
* Optional `maxAtMs` caps the window end (e.g. Estimasi Panen).
*/
export async function loadDeviceHistory24h(
metric: IotDeviceHistoryMetric,
flockId: string | null | undefined
flockId: string | null | undefined,
maxAtMs?: number | null
): Promise<IotDeviceHistoryPoint[]> {
const deviceKey = deviceKeyFromHistoryMetric(metric);
const flockSnapshots = await loadFlockSnapshots24h();
const flockSnapshots = await loadFlockSnapshots24h(maxAtMs);
if (isFanHistoryMetric(metric)) {
const fanNumber = fanNumberFromMetric(metric);
const [statusEvents, setpointEvents] = await Promise.all([
Promise.resolve(statusEventsFromSnapshots(flockSnapshots, deviceKey)),
loadFanSetpointLogEvents24h(String(flockId || '').trim(), fanNumber, flockSnapshots),
loadFanSetpointLogEvents24h(String(flockId || '').trim(), fanNumber, flockSnapshots, maxAtMs),
]);
return mergeDeviceHistoryEvents([...statusEvents, ...setpointEvents]);
}
@@ -304,7 +403,8 @@ export async function loadDeviceHistory24h(
loadHvacSetpointLogEvents24h(
String(flockId || '').trim(),
deviceKey as HvacDeviceKey,
flockSnapshots
flockSnapshots,
maxAtMs
),
]);
return mergeDeviceHistoryEvents([...statusEvents, ...setpointEvents]);
@@ -318,17 +418,19 @@ export async function loadDeviceHistory24h(
*/
export async function loadFanHistory24h(
fanNumber: number,
flockId: string | null | undefined
flockId: string | null | undefined,
maxAtMs?: number | null
): Promise<IotDeviceHistoryPoint[]> {
if (!Number.isInteger(fanNumber) || fanNumber < 1 || fanNumber > 11) return [];
return loadDeviceHistory24h(`fan${fanNumber}` as IotDeviceHistoryMetric, flockId);
return loadDeviceHistory24h(`fan${fanNumber}` as IotDeviceHistoryMetric, flockId, maxAtMs);
}
/** @deprecated Use {@link loadDeviceHistory24h} */
export async function loadFan1History24h(
flockId: string | null | undefined
flockId: string | null | undefined,
maxAtMs?: number | null
): Promise<IotDeviceHistoryPoint[]> {
return loadDeviceHistory24h('fan1', flockId);
return loadDeviceHistory24h('fan1', flockId, maxAtMs);
}
type SetpointLogsPageJson = {
@@ -342,12 +444,13 @@ const MAX_SETPOINT_LOG_PAGES = 50;
* 24h series from `GET .../flock/{id}/setpoint/logs/?page=N` (current setpoint per log row).
*/
export async function loadSetpointHistory24hFromLogs(
flockId: string
flockId: string,
maxAtMs?: number | null
): Promise<IotSensorHistoryPoint[]> {
const safeId = String(flockId || '').trim();
if (!safeId) return [];
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
const { upperMs, cutoffMs } = historyWindowBounds(maxAtMs);
const base = `${IOT_DASHBOARD_IOT_API_BASE}/flock/${encodeURIComponent(safeId)}/setpoint/logs/`;
const points: IotSensorHistoryPoint[] = [];
@@ -371,7 +474,7 @@ export async function loadSetpointHistory24hFromLogs(
const rawAt = row.fetched_at || row.updated_at;
if (!rawAt) continue;
const atMs = new Date(rawAt).getTime();
if (!Number.isFinite(atMs) || atMs < cutoffMs) continue;
if (!Number.isFinite(atMs) || atMs < cutoffMs || atMs > upperMs) continue;
const raw = row.payload?.data?.setpoint_sekarang?.current_setpoint;
const n = Number(raw);
@@ -387,35 +490,44 @@ export async function loadSetpointHistory24hFromLogs(
/**
* Loads 24h history from `/api/iot/flocks/` (same source as panel), except **setPoint**
* which uses per-flock setpoint logs on dashboard CPSP.
* Optional `maxAtMs` caps the window end (e.g. Estimasi Panen).
*/
export async function loadIotSensorHistory24h(
metric: FlockHistoryMetric,
flockId?: string | null
flockId?: string | null,
maxAtMs?: number | null
): Promise<IotSensorHistoryPoint[]> {
if (metric === 'setPoint') {
return loadSetpointHistory24hFromLogs(String(flockId || '').trim());
return loadSetpointHistory24hFromLogs(String(flockId || '').trim(), maxAtMs);
}
const extract = EXTRACTORS[metric];
const now = Date.now();
const cutoffMs = now - 24 * 60 * 60 * 1000;
const { upperMs, cutoffMs } = historyWindowBounds(maxAtMs);
const points: IotSensorHistoryPoint[] = [];
let nextUrl: string | null = `${IOT_FLOCKS_API_URL}?_=${Date.now()}`;
const { startUrl, firstPage } = await resolveFlocksHistoryStart(upperMs);
let nextUrl: string | null = startUrl;
let pendingFirstPage = firstPage;
let safetyPageCount = 0;
const maxPages = 20;
const maxPages = 30;
while (nextUrl && safetyPageCount < maxPages) {
let json: IotFlocksRaw & { next?: string | null };
if (pendingFirstPage) {
json = pendingFirstPage;
pendingFirstPage = null;
} else {
const response = await fetch(nextUrl, {
cache: 'no-store',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error(`Gagal memuat riwayat (${response.status})`);
json = (await response.json()) as IotFlocksRaw & { next?: string | null };
}
const json = (await response.json()) as IotFlocksRaw & { next?: string | null };
const rows = Array.isArray(json.results) ? json.results : [];
if (rows.length === 0) break;
let pageHasNewerThanCutoff = false;
let pageHasDataInOrAfterWindow = false;
for (const row of rows) {
const payloadData = (row as IotFlockResultRow)?.payload?.data;
@@ -426,16 +538,20 @@ export async function loadIotSensorHistory24h(
const atMs = new Date(rawAt).getTime();
if (!Number.isFinite(atMs)) continue;
if (atMs > upperMs) {
pageHasDataInOrAfterWindow = true;
continue;
}
if (atMs < cutoffMs) continue;
pageHasNewerThanCutoff = true;
pageHasDataInOrAfterWindow = true;
const normalized = preprocessFlockPayloadForPanel(payloadData);
const value = extract(normalized);
if (value === null) continue;
points.push({ atMs, value: Number(value) });
}
if (!pageHasNewerThanCutoff) break;
if (!pageHasDataInOrAfterWindow) break;
const nextFromApi = json.next ?? null;
nextUrl = nextFromApi ? nextFromApi.replace('http://', 'https://') : null;
+6 -9
View File
@@ -85,7 +85,6 @@ import { cobbStandardPerformance } from '../mockData/cobbStandard.ts';
import {
shouldUsePreviousDay,
getEffectiveDate,
getAdjustedDateRange,
formatDateForDb,
formatEffectiveDateLabel,
} from '../utils/dateFilter.ts';
@@ -876,16 +875,15 @@ export const AppContextProvider: React.FC<AppContextProviderProps> = ({
}
try {
// Use the selected date for API call with 14-day caching
const dateStr = staticDataStore.distributionSelectedDate.toISOString().split('T')[0];
const adjustedRange = getAdjustedDateRange(dateStr, dateStr);
// Use the calendar-selected date as-is (no D-1 override for explicit user picks)
const dateStr = formatDateForDb(staticDataStore.distributionSelectedDate);
const cycleStartStr =
activeCycle.startDate instanceof Date
? activeCycle.startDate.toISOString().split('T')[0]
: activeCycle.startDate;
const response: WeightDistributionApiResponse = await apiClient.getWeightDistributionRange(
adjustedRange.start,
adjustedRange.end,
dateStr,
dateStr,
'LIVE',
cycleStartStr
);
@@ -947,9 +945,8 @@ export const AppContextProvider: React.FC<AppContextProviderProps> = ({
const fetchSensorWeightSummary = useCallback(async () => {
staticDataStore.setDailyDataLoading(true);
try {
const dateStr = staticDataStore.distributionSelectedDate.toISOString().split('T')[0];
const adjustedRange = getAdjustedDateRange(dateStr, dateStr);
const response = await apiClient.getSensorWeightSummary(adjustedRange.start, 'LIVE');
const dateStr = formatDateForDb(staticDataStore.distributionSelectedDate);
const response = await apiClient.getSensorWeightSummary(dateStr, 'LIVE');
if (response.success) {
staticDataStore.setSensorWeightSummary(response.data);
+36
View File
@@ -186,3 +186,39 @@ export function getAdjustedDateRange(
return { start: startDate, end: endDate };
}
type CycleEstimasiPanenInput = {
startDate?: Date | string | null;
endDate?: Date | string | null;
totalDays?: number | null;
};
/**
* Estimasi Panen calendar date: endDate, or startDate + (totalDays - 1).
*/
export function getEstimasiPanenDate(cycle: CycleEstimasiPanenInput): Date | null {
if (cycle.endDate) {
const end = new Date(cycle.endDate);
if (!Number.isFinite(end.getTime())) return null;
end.setHours(0, 0, 0, 0);
return end;
}
if (!cycle.startDate) return null;
const start = new Date(cycle.startDate);
if (!Number.isFinite(start.getTime())) return null;
start.setHours(0, 0, 0, 0);
start.setDate(start.getDate() + Math.max(0, (cycle.totalDays || 1) - 1));
return start;
}
/**
* Inclusive end-of-day timestamp (local) for Estimasi Panen.
* IoT readings with updatedAt after this should not be shown.
*/
export function getEstimasiPanenEndMs(cycle: CycleEstimasiPanenInput): number | null {
const panenDate = getEstimasiPanenDate(cycle);
if (!panenDate) return null;
panenDate.setHours(23, 59, 59, 999);
return panenDate.getTime();
}