fix hitung ayam dan karung data yang ditampilkan melebihi estimasi panen
CI / lint-and-test (push) Canceled after 0s
CI / lint-and-test (push) Canceled after 0s
This commit is contained in:
1 parent
32a36cceff
commit
b6117ae7d5
8 files changed
+104
-48
No files matched your search
@@ -95,8 +95,10 @@ class Cycle {
|
|||||||
const diffMs = today - startDate;
|
const diffMs = today - startDate;
|
||||||
const daysDiff = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const daysDiff = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// Use calculated day, but don't exceed totalDays
|
// 0-based day index: day 0 = startDate, last day = startDate + (totalDays - 1)
|
||||||
calculatedCurrentDay = Math.min(daysDiff, cycle.total_days);
|
// 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 {
|
return {
|
||||||
|
|||||||
@@ -81,8 +81,10 @@ const ChickenCountingPage: React.FC = () => {
|
|||||||
const startDate = new Date(cycleData.startDate);
|
const startDate = new Date(cycleData.startDate);
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
const currentDayOfCycle = Math.floor(
|
const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1);
|
||||||
(today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
|
const currentDayOfCycle = Math.min(
|
||||||
|
Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)),
|
||||||
|
lastCycleDay
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordMap = new Map<number, MortalitySourceRecord>();
|
const recordMap = new Map<number, MortalitySourceRecord>();
|
||||||
@@ -118,7 +120,7 @@ const ChickenCountingPage: React.FC = () => {
|
|||||||
|
|
||||||
return { chickenCounts, populations };
|
return { chickenCounts, populations };
|
||||||
},
|
},
|
||||||
[cycleData.startDate]
|
[cycleData.startDate, cycleData.totalDays]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch all data: total + per-kandang
|
// Fetch all data: total + per-kandang
|
||||||
|
|||||||
@@ -60,6 +60,29 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
useState<FeedSackColumnConfig[]>(DEFAULT_FEED_SACK_COLUMNS);
|
useState<FeedSackColumnConfig[]>(DEFAULT_FEED_SACK_COLUMNS);
|
||||||
const [initialBalance, setInitialBalance] = useState<CycleFeedInitialBalance | null>(null);
|
const [initialBalance, setInitialBalance] = useState<CycleFeedInitialBalance | null>(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 displayedKpis = useMemo(() => {
|
||||||
const karungKpi = kpis.karungDituang;
|
const karungKpi = kpis.karungDituang;
|
||||||
const reportedDay = Math.max(0, Math.round(kpis.umur?.value || 0));
|
const reportedDay = Math.max(0, Math.round(kpis.umur?.value || 0));
|
||||||
@@ -205,7 +228,7 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
columnConfig={columnConfig}
|
columnConfig={columnConfig}
|
||||||
startDate={new Date(cycleData.startDate)}
|
startDate={new Date(cycleData.startDate)}
|
||||||
saldoAwal={initialBalance?.saldoAwal || 0}
|
saldoAwal={initialBalance?.saldoAwal || 0}
|
||||||
effectiveDate={effectiveDate}
|
effectiveDate={feedSaldoEffectiveDate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Chicken Counting Dashboard */}
|
{/* Chicken Counting Dashboard */}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
CycleFeedInitialBalance,
|
CycleFeedInitialBalance,
|
||||||
} from '../types/counting.ts';
|
} from '../types/counting.ts';
|
||||||
import { normalizeFeedSackColumnConfigs } from '../utils/feedSackMapping.ts';
|
import { normalizeFeedSackColumnConfigs } from '../utils/feedSackMapping.ts';
|
||||||
|
import { getEffectiveCycleDay } from '../utils/dateFilter.ts';
|
||||||
import { apiClient } from '../services/apiService.ts';
|
import { apiClient } from '../services/apiService.ts';
|
||||||
import { View, CycleData } from '../types.ts';
|
import { View, CycleData } from '../types.ts';
|
||||||
import { PageAiInsight } from './shared/PageAiInsight.tsx';
|
import { PageAiInsight } from './shared/PageAiInsight.tsx';
|
||||||
@@ -86,28 +87,42 @@ const FeedSackCountingPage: React.FC<FeedSackCountingPageProps> = ({ setView })
|
|||||||
}, [cycles, selectedCycleId, cycleData]);
|
}, [cycles, selectedCycleId, cycleData]);
|
||||||
|
|
||||||
// Compute the display end date for the selected cycle.
|
// 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 displayDate = useMemo(() => {
|
||||||
const start = new Date(selectedCycle.startDate);
|
const start = new Date(selectedCycle.startDate);
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const end = new Date(start);
|
const end = new Date(start);
|
||||||
if (selectedCycle.status === 'Active') {
|
end.setDate(end.getDate() + displayCurrentDay);
|
||||||
// `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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return end;
|
return end;
|
||||||
}, [selectedCycle.currentDay, selectedCycle.startDate, selectedCycle.status, effectiveDate]);
|
}, [selectedCycle.startDate, displayCurrentDay]);
|
||||||
|
|
||||||
// Compute date range from selected cycle
|
// Compute date range from selected cycle
|
||||||
const { startDate, endDate } = useMemo(() => {
|
const { startDate, endDate } = useMemo(() => {
|
||||||
@@ -163,16 +178,6 @@ const FeedSackCountingPage: React.FC<FeedSackCountingPageProps> = ({ setView })
|
|||||||
});
|
});
|
||||||
}, [feedSackHistory, startDate, endDate]);
|
}, [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
|
// Handler for saving initial balance
|
||||||
const handleSaveInitialBalance = useCallback(
|
const handleSaveInitialBalance = useCallback(
|
||||||
async (saldoAwal: number) => {
|
async (saldoAwal: number) => {
|
||||||
@@ -369,13 +374,25 @@ const FeedSackCountingPage: React.FC<FeedSackCountingPageProps> = ({ setView })
|
|||||||
hari_ke: displayCurrentDay,
|
hari_ke: displayCurrentDay,
|
||||||
total_catatan_iot: filteredData.length,
|
total_catatan_iot: filteredData.length,
|
||||||
saldo_awal_karung: initialBalance?.saldoAwal ?? null,
|
saldo_awal_karung: initialBalance?.saldoAwal ?? null,
|
||||||
total_karung_masuk_iot: filteredData.reduce((sum: number, d: any) => sum + (d.countIn ?? 0), 0),
|
total_karung_masuk_iot: filteredData.reduce(
|
||||||
total_karung_dituang_iot: filteredData.reduce((sum: number, d: any) => sum + (d.countOut ?? 0), 0),
|
(sum: number, d: any) => sum + (d.countIn ?? 0),
|
||||||
rata_karung_per_hari: displayCurrentDay > 0
|
0
|
||||||
? Math.round(filteredData.reduce((sum: number, d: any) => sum + (d.countOut ?? 0), 0) / displayCurrentDay * 10) / 10
|
),
|
||||||
|
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,
|
: null,
|
||||||
total_entry_manual: manualFeedSackEntries.length,
|
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<FeedSackCountingPageProps> = ({ setView })
|
|||||||
|
|
||||||
{/* Saldo Karung Card */}
|
{/* Saldo Karung Card */}
|
||||||
<FeedSaldoCard
|
<FeedSaldoCard
|
||||||
iotData={feedSackHistory}
|
iotData={filteredData}
|
||||||
columnConfig={columnConfig}
|
columnConfig={columnConfig}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
saldoAwal={initialBalance?.saldoAwal || 0}
|
saldoAwal={initialBalance?.saldoAwal || 0}
|
||||||
|
|||||||
@@ -178,8 +178,10 @@ const ChickenCountingDashboard: React.FC<ChickenCountingDashboardProps> = ({
|
|||||||
const startDate = new Date(cycleData.startDate);
|
const startDate = new Date(cycleData.startDate);
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
const currentDayOfCycle = Math.floor(
|
const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1);
|
||||||
(today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
|
const currentDayOfCycle = Math.min(
|
||||||
|
Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)),
|
||||||
|
lastCycleDay
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordMap = new Map<number, MortalitySourceRecord>();
|
const recordMap = new Map<number, MortalitySourceRecord>();
|
||||||
@@ -213,7 +215,7 @@ const ChickenCountingDashboard: React.FC<ChickenCountingDashboardProps> = ({
|
|||||||
|
|
||||||
return { chickenCounts, populations };
|
return { chickenCounts, populations };
|
||||||
},
|
},
|
||||||
[cycleData.startDate]
|
[cycleData.startDate, cycleData.totalDays]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch all data: total + per-kandang
|
// Fetch all data: total + per-kandang
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ const CombinedTrendChart: React.FC<CombinedTrendChartProps> = ({
|
|||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
const startDateMs = startDate.getTime();
|
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();
|
const recordMap = new Map();
|
||||||
sortedRecords.forEach((record: any) => {
|
sortedRecords.forEach((record: any) => {
|
||||||
|
|||||||
@@ -200,7 +200,9 @@ const DataManagementPage: React.FC<DataManagementPageProps> = ({ selectedKandang
|
|||||||
const diffMs = todayMs - startDateMs;
|
const diffMs = todayMs - startDateMs;
|
||||||
const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0;
|
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
|
// Fetch chicken counting data
|
||||||
const chickenCountMap: Record<number, number> = {};
|
const chickenCountMap: Record<number, number> = {};
|
||||||
@@ -249,6 +251,7 @@ const DataManagementPage: React.FC<DataManagementPageProps> = ({ selectedKandang
|
|||||||
cycleData.id,
|
cycleData.id,
|
||||||
cycleData.startDate,
|
cycleData.startDate,
|
||||||
cycleData.currentDay,
|
cycleData.currentDay,
|
||||||
|
cycleData.totalDays,
|
||||||
selectedKandangId,
|
selectedKandangId,
|
||||||
initialPopulation,
|
initialPopulation,
|
||||||
calculateRecords,
|
calculateRecords,
|
||||||
@@ -284,7 +287,9 @@ const DataManagementPage: React.FC<DataManagementPageProps> = ({ selectedKandang
|
|||||||
const diffMs = todayMs - startDateMs;
|
const diffMs = todayMs - startDateMs;
|
||||||
const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0;
|
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
|
// Fetch chicken counting data
|
||||||
const chickenCountMap: Record<number, number> = {};
|
const chickenCountMap: Record<number, number> = {};
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ const FeedTrendChart: React.FC<FeedTrendChartProps> = ({
|
|||||||
const cycleStart = new Date(cycleStartDate);
|
const cycleStart = new Date(cycleStartDate);
|
||||||
cycleStart.setHours(0, 0, 0, 0);
|
cycleStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const now = new Date();
|
const cycleEnd = new Date(cycleStart);
|
||||||
now.setHours(0, 0, 0, 0);
|
cycleEnd.setDate(cycleStart.getDate() + currentDay);
|
||||||
|
cycleEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
// Initialize daily buckets
|
// Initialize daily buckets
|
||||||
const dailyData: { [key: number]: Record<string, any> } = {};
|
const dailyData: { [key: number]: Record<string, any> } = {};
|
||||||
@@ -49,7 +50,7 @@ const FeedTrendChart: React.FC<FeedTrendChartProps> = ({
|
|||||||
|
|
||||||
// Aggregate data by day
|
// Aggregate data by day
|
||||||
data.forEach((log) => {
|
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 diffTime = log.timestamp.getTime() - cycleStart.getTime();
|
||||||
const dayNumber = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
const dayNumber = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user