diff --git a/backend/models/Cycle.js b/backend/models/Cycle.js index a53f34b..5e4e55e 100644 --- a/backend/models/Cycle.js +++ b/backend/models/Cycle.js @@ -95,8 +95,10 @@ class Cycle { const diffMs = today - startDate; const daysDiff = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - // Use calculated day, but don't exceed totalDays - calculatedCurrentDay = Math.min(daysDiff, cycle.total_days); + // 0-based day index: day 0 = startDate, last day = startDate + (totalDays - 1) + // Cap at totalDays - 1 so currentDay never maps past Estimasi Panen / end_date + const lastDayIndex = Math.max(0, cycle.total_days - 1); + calculatedCurrentDay = Math.min(daysDiff, lastDayIndex); } return { diff --git a/components/ChickenCountingPage.tsx b/components/ChickenCountingPage.tsx index 5383c87..fcf03e6 100644 --- a/components/ChickenCountingPage.tsx +++ b/components/ChickenCountingPage.tsx @@ -81,8 +81,10 @@ const ChickenCountingPage: React.FC = () => { const startDate = new Date(cycleData.startDate); today.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0); - const currentDayOfCycle = Math.floor( - (today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24) + const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); + const currentDayOfCycle = Math.min( + Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)), + lastCycleDay ); const recordMap = new Map(); @@ -118,7 +120,7 @@ const ChickenCountingPage: React.FC = () => { return { chickenCounts, populations }; }, - [cycleData.startDate] + [cycleData.startDate, cycleData.totalDays] ); // Fetch all data: total + per-kandang diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index ff17ca6..81a4e8f 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -60,6 +60,29 @@ const Dashboard: React.FC = ({ useState(DEFAULT_FEED_SACK_COLUMNS); const [initialBalance, setInitialBalance] = useState(null); + const feedSaldoEffectiveDate = useMemo(() => { + const cappedEffectiveDate = new Date(effectiveDate); + cappedEffectiveDate.setHours(0, 0, 0, 0); + + if (!cycleData.startDate) { + return cappedEffectiveDate; + } + + const cycleEndDate = cycleData.endDate + ? new Date(cycleData.endDate) + : (() => { + const derivedEndDate = new Date(cycleData.startDate); + derivedEndDate.setDate( + derivedEndDate.getDate() + Math.max(0, (cycleData.totalDays || 1) - 1) + ); + return derivedEndDate; + })(); + + cycleEndDate.setHours(0, 0, 0, 0); + + return cappedEffectiveDate <= cycleEndDate ? cappedEffectiveDate : cycleEndDate; + }, [cycleData.endDate, cycleData.startDate, cycleData.totalDays, effectiveDate]); + const displayedKpis = useMemo(() => { const karungKpi = kpis.karungDituang; const reportedDay = Math.max(0, Math.round(kpis.umur?.value || 0)); @@ -205,7 +228,7 @@ const Dashboard: React.FC = ({ columnConfig={columnConfig} startDate={new Date(cycleData.startDate)} saldoAwal={initialBalance?.saldoAwal || 0} - effectiveDate={effectiveDate} + effectiveDate={feedSaldoEffectiveDate} /> {/* Chicken Counting Dashboard */} diff --git a/components/FeedSackCountingPage.tsx b/components/FeedSackCountingPage.tsx index a068884..20c32d8 100644 --- a/components/FeedSackCountingPage.tsx +++ b/components/FeedSackCountingPage.tsx @@ -14,6 +14,7 @@ import { CycleFeedInitialBalance, } from '../types/counting.ts'; import { normalizeFeedSackColumnConfigs } from '../utils/feedSackMapping.ts'; +import { getEffectiveCycleDay } from '../utils/dateFilter.ts'; import { apiClient } from '../services/apiService.ts'; import { View, CycleData } from '../types.ts'; import { PageAiInsight } from './shared/PageAiInsight.tsx'; @@ -86,28 +87,42 @@ const FeedSackCountingPage: React.FC = ({ setView }) }, [cycles, selectedCycleId, cycleData]); // Compute the display end date for the selected cycle. - // Active cycle follows the homepage D-1 rule; completed cycles keep their stored day. + // Active cycle follows the homepage D-1 rule; completed cycles use endDate/currentDay. + // Always cap at totalDays - 1 so data never extends past Estimasi Panen. + const displayCurrentDay = useMemo(() => { + const lastCycleDay = Math.max(0, (selectedCycle.totalDays || 1) - 1); + + if (selectedCycle.status === 'Active') { + const rawDay = Math.max(0, getEffectiveCycleDay(selectedCycle.startDate)); + return Math.min(rawDay, lastCycleDay); + } + + if (selectedCycle.endDate) { + const start = new Date(selectedCycle.startDate); + start.setHours(0, 0, 0, 0); + const cycleEnd = new Date(selectedCycle.endDate); + cycleEnd.setHours(0, 0, 0, 0); + const dayFromEnd = Math.floor((cycleEnd.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + return Math.min(Math.max(0, dayFromEnd), lastCycleDay); + } + + return Math.min(Math.max(0, selectedCycle.currentDay ?? 0), lastCycleDay); + }, [ + selectedCycle.currentDay, + selectedCycle.endDate, + selectedCycle.startDate, + selectedCycle.status, + selectedCycle.totalDays, + effectiveDate, + ]); + const displayDate = useMemo(() => { const start = new Date(selectedCycle.startDate); start.setHours(0, 0, 0, 0); - const end = new Date(start); - if (selectedCycle.status === 'Active') { - // `effectiveDate` from AppContext is already D-1 adjusted, so we only - // compute the cycle-day offset here without applying the cutoff logic again. - const effectiveDateOnly = new Date(effectiveDate); - effectiveDateOnly.setHours(0, 0, 0, 0); - const effectiveCurrentDay = Math.max( - 0, - Math.floor((effectiveDateOnly.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) - ); - end.setDate(end.getDate() + effectiveCurrentDay); - } else { - end.setDate(end.getDate() + selectedCycle.currentDay); - } - + end.setDate(end.getDate() + displayCurrentDay); return end; - }, [selectedCycle.currentDay, selectedCycle.startDate, selectedCycle.status, effectiveDate]); + }, [selectedCycle.startDate, displayCurrentDay]); // Compute date range from selected cycle const { startDate, endDate } = useMemo(() => { @@ -163,16 +178,6 @@ const FeedSackCountingPage: React.FC = ({ setView }) }); }, [feedSackHistory, startDate, endDate]); - // Calculate currentDay from selected cycle - const displayCurrentDay = useMemo(() => { - const start = new Date(selectedCycle.startDate); - start.setHours(0, 0, 0, 0); - const end = new Date(displayDate); - end.setHours(0, 0, 0, 0); - const diffDays = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - return Math.max(0, diffDays); - }, [selectedCycle.startDate, displayDate]); - // Handler for saving initial balance const handleSaveInitialBalance = useCallback( async (saldoAwal: number) => { @@ -369,13 +374,25 @@ const FeedSackCountingPage: React.FC = ({ setView }) hari_ke: displayCurrentDay, total_catatan_iot: filteredData.length, saldo_awal_karung: initialBalance?.saldoAwal ?? null, - total_karung_masuk_iot: filteredData.reduce((sum: number, d: any) => sum + (d.countIn ?? 0), 0), - total_karung_dituang_iot: filteredData.reduce((sum: number, d: any) => sum + (d.countOut ?? 0), 0), - rata_karung_per_hari: displayCurrentDay > 0 - ? Math.round(filteredData.reduce((sum: number, d: any) => sum + (d.countOut ?? 0), 0) / displayCurrentDay * 10) / 10 - : null, + total_karung_masuk_iot: filteredData.reduce( + (sum: number, d: any) => sum + (d.countIn ?? 0), + 0 + ), + total_karung_dituang_iot: filteredData.reduce( + (sum: number, d: any) => sum + (d.countOut ?? 0), + 0 + ), + rata_karung_per_hari: + displayCurrentDay > 0 + ? Math.round( + (filteredData.reduce((sum: number, d: any) => sum + (d.countOut ?? 0), 0) / + displayCurrentDay) * + 10 + ) / 10 + : null, total_entry_manual: manualFeedSackEntries.length, - catatan: 'Karung masuk = stok diterima, karung dituang = pakan dikonsumsi oleh ayam.', + catatan: + 'Karung masuk = stok diterima, karung dituang = pakan dikonsumsi oleh ayam.', }} /> )} @@ -396,7 +413,7 @@ const FeedSackCountingPage: React.FC = ({ setView }) {/* Saldo Karung Card */} = ({ const startDate = new Date(cycleData.startDate); today.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0); - const currentDayOfCycle = Math.floor( - (today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24) + const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); + const currentDayOfCycle = Math.min( + Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)), + lastCycleDay ); const recordMap = new Map(); @@ -213,7 +215,7 @@ const ChickenCountingDashboard: React.FC = ({ return { chickenCounts, populations }; }, - [cycleData.startDate] + [cycleData.startDate, cycleData.totalDays] ); // Fetch all data: total + per-kandang diff --git a/components/counting/CombinedTrendChart.tsx b/components/counting/CombinedTrendChart.tsx index 4b3d693..5d83b0c 100644 --- a/components/counting/CombinedTrendChart.tsx +++ b/components/counting/CombinedTrendChart.tsx @@ -83,7 +83,11 @@ const CombinedTrendChart: React.FC = ({ today.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0); const startDateMs = startDate.getTime(); - const currentDayOfCycle = Math.floor((today.getTime() - startDateMs) / (1000 * 60 * 60 * 24)); + const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); + const currentDayOfCycle = Math.min( + Math.floor((today.getTime() - startDateMs) / (1000 * 60 * 60 * 24)), + lastCycleDay + ); const recordMap = new Map(); sortedRecords.forEach((record: any) => { diff --git a/components/counting/DataManagementPage.tsx b/components/counting/DataManagementPage.tsx index 493a141..b93fb1f 100644 --- a/components/counting/DataManagementPage.tsx +++ b/components/counting/DataManagementPage.tsx @@ -200,7 +200,9 @@ const DataManagementPage: React.FC = ({ selectedKandang const diffMs = todayMs - startDateMs; const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0; - const maxDay = cycleData.currentDay; + // Last inclusive 0-based day index matches Estimasi Panen (startDate + totalDays - 1) + const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); + const maxDay = Math.min(cycleData.currentDay ?? 0, lastCycleDay); // Fetch chicken counting data const chickenCountMap: Record = {}; @@ -249,6 +251,7 @@ const DataManagementPage: React.FC = ({ selectedKandang cycleData.id, cycleData.startDate, cycleData.currentDay, + cycleData.totalDays, selectedKandangId, initialPopulation, calculateRecords, @@ -284,7 +287,9 @@ const DataManagementPage: React.FC = ({ selectedKandang const diffMs = todayMs - startDateMs; const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0; - const maxDay = cycleData.currentDay; + // Last inclusive 0-based day index matches Estimasi Panen (startDate + totalDays - 1) + const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); + const maxDay = Math.min(cycleData.currentDay ?? 0, lastCycleDay); // Fetch chicken counting data const chickenCountMap: Record = {}; diff --git a/components/feedSack/FeedTrendChart.tsx b/components/feedSack/FeedTrendChart.tsx index 2ac89eb..9e41945 100644 --- a/components/feedSack/FeedTrendChart.tsx +++ b/components/feedSack/FeedTrendChart.tsx @@ -30,8 +30,9 @@ const FeedTrendChart: React.FC = ({ const cycleStart = new Date(cycleStartDate); cycleStart.setHours(0, 0, 0, 0); - const now = new Date(); - now.setHours(0, 0, 0, 0); + const cycleEnd = new Date(cycleStart); + cycleEnd.setDate(cycleStart.getDate() + currentDay); + cycleEnd.setHours(23, 59, 59, 999); // Initialize daily buckets const dailyData: { [key: number]: Record } = {}; @@ -49,7 +50,7 @@ const FeedTrendChart: React.FC = ({ // Aggregate data by day data.forEach((log) => { - if (log.timestamp >= cycleStart && log.timestamp <= now) { + if (log.timestamp >= cycleStart && log.timestamp <= cycleEnd) { const diffTime = log.timestamp.getTime() - cycleStart.getTime(); const dayNumber = Math.floor(diffTime / (1000 * 60 * 60 * 24));