import React, { createContext, useState, useCallback, useContext, ReactNode, useEffect, useMemo, } from 'react'; // Import types import { Kpi, Alert, CoopZone, EnvironmentData, Camera, PopulationDataPoint, LiveCount, FeedSackLogEntry, ManualFeedSackEntry, BehaviorMetrics, BehaviorDataPoint, BehaviorAnomaly, WeightStatistics, WeightDataPoint, WeightDistributionPoint, WeightAlert, IotDevice, Location, Coop, Floor, AggregatedCounts, NetworkDevice, BandwidthHistoryPoint, BatchService, BatchJob, User, Group, Role, Permission, RolePermission, CycleData, MultiCoopChartPoint, MultiCoopWeightSummary, FeedWastageStats, WastageHotspot, FeedWastageAlert, WastageDataPoint, FeedScheduleRecommendation, FeedingEvent, ConsumptionPatternPoint, ScheduleInfluencingFactors, ErpSyncStats, ErpEndpoint, VetVisit, OcrDocument, ManualWeighingRecord, AiRecommendation, RecommendationStatus, CkaleWeightPoint, FcrDataPoint, WeightDistributionApiResponse, WeightDistributionStats, } from '../types.ts'; // Import custom store hooks import { useAccessManagementStore } from './stores/useAccessManagementStore.ts'; import { useLocationStore } from './stores/useLocationStore.ts'; import { useDeviceStore } from './stores/useDeviceStore.ts'; import { useStaticDataStore } from './stores/useStaticDataStore.ts'; // Import refactored logic hooks import { useCrossStoreHandlers } from './hooks/useCrossStoreHandlers.ts'; import { useDerivedState } from './hooks/useDerivedState.ts'; import { useLocationHelpers } from './hooks/useLocationHelpers.ts'; import { apiClient } from '../services/apiService.ts'; import { processDailyWeightData, calculateDailyWeightGain, processDeviceWeightData, } from '../utils/scaleDataProcessor.ts'; import { cobbStandardPerformance } from '../mockData/cobbStandard.ts'; import { shouldUsePreviousDay, getEffectiveDate, getAdjustedDateRange, formatDateForDb, formatEffectiveDateLabel, } from '../utils/dateFilter.ts'; interface AppContextType { // State kpis: Record; alerts: Alert[]; coopZones: CoopZone[]; environmentData: EnvironmentData[]; locations: Location[]; coops: Coop[]; floors: Floor[]; cameras: Camera[]; populationHistory: PopulationDataPoint[]; liveCounts: LiveCount[]; aggregatedCounts: AggregatedCounts; feedSackHistory: FeedSackLogEntry[]; availableFeedSackCameras: string[]; manualFeedSackEntries: ManualFeedSackEntry[]; behaviorMetrics: BehaviorMetrics; behaviorHistory: BehaviorDataPoint[]; behaviorAnomalies: BehaviorAnomaly[]; weightStats: WeightStatistics; weightHistory: WeightDataPoint[]; weightDistribution: WeightDistributionPoint[]; weightAlerts: WeightAlert[]; iotDevices: IotDevice[]; networkDevices: NetworkDevice[]; bandwidthHistory: BandwidthHistoryPoint[]; batchServices: BatchService[]; batchJobs: BatchJob[]; users: User[]; groups: Group[]; roles: Role[]; permissions: Permission[]; rolePermissions: RolePermission[]; currentUser: User | null; cycleData: CycleData; // The active cycle cycles: CycleData[]; // All cycles multiCoopChartData: MultiCoopChartPoint[]; multiCoopSummaryData: MultiCoopWeightSummary[]; coopNamesForChart: string[]; feedWastageStats: FeedWastageStats; wastageHotspots: WastageHotspot[]; wastageAlerts: FeedWastageAlert[]; wastageHistory: WastageDataPoint[]; feedSchedule: FeedScheduleRecommendation; feedingEvents: FeedingEvent[]; consumptionPattern: ConsumptionPatternPoint[]; scheduleFactors: ScheduleInfluencingFactors; erpSyncStats: ErpSyncStats; erpEndpoints: ErpEndpoint[]; vetVisits: VetVisit[]; ocrDocuments: OcrDocument[]; manualWeighingData: ManualWeighingRecord[]; aiRecommendations: AiRecommendation[]; ckaleData: CkaleWeightPoint[]; fcrComparisonData: FcrDataPoint[]; isLoadingData: boolean; isRefreshingWeightKpi: boolean; distributionSelectedDate: Date; distributionStats: WeightDistributionStats | null; apiMode: 'LIVE' | 'DEMO'; dailyWeightData: any[]; // DailyWeightPoint[] dailyWeightGainData: any[]; // DailyWeightGainPoint[] deviceWeightData: any[]; // DeviceWeightData[] scaleDataLoading: boolean; scaleDataLastUpdate: Date; dailyDataLoading: boolean; feedSackDataLastUpdate: Date | null; isFeedSackDataLoading: boolean; effectiveDate: Date; isShowingPreviousDayData: boolean; // Auth & Permissions login: (user: User) => void; logout: () => void; hasPermission: (permissionId: string) => boolean; // Handlers onAddLocation: (name: string) => void; onUpdateLocation: (location: Location) => void; onDeleteLocation: (locationId: string) => void; onAddCoop: (name: string, locationId: string) => void; onUpdateCoop: (coop: Coop) => void; onDeleteCoop: (coopId: string) => void; onAddFloor: (name: string, coopId: string) => void; onUpdateFloor: (floor: Floor) => void; onDeleteFloor: (floorId: string) => void; onAddCamera: (cameraData: Omit) => void; onUpdateCamera: (camera: Camera) => void; onDeleteCamera: (cameraId: string) => void; onAddIotDevice: ( deviceData: Omit ) => void; onUpdateIotDevice: (device: IotDevice) => void; onDeleteIotDevice: (deviceId: string) => void; onAddUser: (userData: Omit) => void; onUpdateUser: (user: User) => void; onDeleteUser: (userId: string) => void; onTogglePermission: (roleId: string, permissionId: string) => void; onUpdateVetVisit: (updatedVisit: VetVisit) => void; onUpdateOcrDocument: (updatedDocument: OcrDocument) => void; onAddFeedingEvent: (eventData: Omit) => void; onUpdateFeedingEvent: (event: FeedingEvent) => void; onAddVetVisit: (visitData: Omit) => void; onUpdateManualWeighingRecord: (updatedRecord: ManualWeighingRecord) => void; onUpdateRecommendationStatus: (id: string, status: RecommendationStatus) => void; onUpdateCycleDates: ( cycleId: string, startDate: Date, endDate: Date, chickInWeight?: number, docInCount?: number ) => void; onCreateCycle: (startDate: Date, docInCount?: number, chickInWeight?: number) => void; setDistributionSelectedDate: (date: Date) => void; fetchWeightDistribution: () => Promise; fetchFeedSackData: (forceRefresh?: boolean) => Promise; fetchManualFeedSackData: (startDate: string, endDate: string) => Promise; refreshFeedSackData: () => Promise; fetchScaleData: () => Promise; refreshScaleData: () => Promise; // Helpers getFullLocationName: (floorId: string | null) => string; getNetworkDeviceLocationName: (device: NetworkDevice) => string; } const AppContext = createContext(undefined); interface AppContextProviderProps { children: ReactNode; selectedKandangId: number | null; } export const AppContextProvider: React.FC = ({ children, selectedKandangId, }) => { // --- Custom store hooks --- const accessManagementStore = useAccessManagementStore(); const locationStore = useLocationStore(); const deviceStore = useDeviceStore(); const staticDataStore = useStaticDataStore(); // --- Authentication State --- const [currentUser, setCurrentUser] = useState(null); // Default to logged out const [isLoadingData, setIsLoadingData] = useState(false); // --- Kandang State --- const [kandangs, setKandangs] = useState([]); // Store kandangs to access cage_uuid const [isRefreshingWeightKpi, setIsRefreshingWeightKpi] = useState(false); // --- Feed Sack Cache State --- const [feedSackDataLastUpdate, setFeedSackDataLastUpdate] = useState(null); const [isFeedSackDataLoading, setIsFeedSackDataLoading] = useState(false); // --- D-1 Filter State (Time-Based Data Filtering) --- const [effectiveDate, setEffectiveDate] = useState(new Date()); const [isShowingPreviousDayData, setIsShowingPreviousDayData] = useState(false); // --- Refactored Hooks --- const crossStoreHandlers = useCrossStoreHandlers(locationStore, deviceStore); const derivedState = useDerivedState(deviceStore, staticDataStore, locationStore); const locationHelpers = useLocationHelpers(locationStore); const getCycleDayNumber = useCallback((cycleStart: Date, targetDate: Date): number => { const start = new Date(cycleStart); const target = new Date(targetDate); start.setHours(0, 0, 0, 0); target.setHours(0, 0, 0, 0); const diffMs = target.getTime() - start.getTime(); if (diffMs < 0) { return -1; } return Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; }, []); type DashboardKpiRow = { mortality?: number; food?: number; food_total?: number; chicken_life?: number; ref_weight?: number; manual_weight?: number; manual_fcr?: number; iot_weight?: number; iot_fcr?: number; iot_eef?: number; manual_eef?: number; iot_average_day_weight?: number; manual_average_day_weight?: number; }; const getSortedKpiKeys = useCallback((apiKpiData: Record): number[] => { return Object.keys(apiKpiData) .map((key) => parseInt(key, 10)) .filter((key) => Number.isFinite(key)) .sort((a, b) => a - b); }, []); const getLatestApiDayAge = useCallback((sortedKeys: number[]): number => { if (sortedKeys.length === 0) { return 0; } return sortedKeys[sortedKeys.length - 1]; }, []); const resolveDashboardKpiSelection = useCallback( (apiKpiData: Record, cycleStart: Date, reportDate: Date) => { const reportingDay = getCycleDayNumber(cycleStart, reportDate); const desiredKey = Math.max(0, reportingDay - 1); const sortedKeys = getSortedKpiKeys(apiKpiData); const fallbackKey = sortedKeys.length > 0 ? sortedKeys[sortedKeys.length - 1] : desiredKey; const selectedKey: number = apiKpiData[desiredKey.toString()] ? desiredKey : (fallbackKey ?? desiredKey); const selectedDayData = apiKpiData[selectedKey.toString()] || null; return { reportingDay, desiredKey, selectedKey, selectedDayData, sortedKeys, }; }, [getCycleDayNumber, getSortedKpiKeys] ); // Helper function to get cage UUID for selected kandang const getCurrentCageUuid = useCallback((): string | null => { if (selectedKandangId === null || kandangs.length === 0) return null; const kandang = kandangs.find((k: any) => k.id === selectedKandangId); return kandang?.cageUuid || null; }, [selectedKandangId, kandangs]); const activeCycle = useMemo(() => { const active = staticDataStore.cycles.find((c) => c.status === 'Active'); if (active) { return active; } // Fallback: If no active cycle, use the latest cycle (by start date) if (staticDataStore.cycles.length === 0) { console.error('❌ NO CYCLES FOUND! Database may be empty.'); return staticDataStore.cycles[0]; // Return undefined if no cycles } const latestCycle = [...staticDataStore.cycles].sort((a, b) => { const dateA = a.startDate instanceof Date ? a.startDate : new Date(a.startDate); const dateB = b.startDate instanceof Date ? b.startDate : new Date(b.startDate); return dateB.getTime() - dateA.getTime(); // Sort descending (latest first) })[0]; if (latestCycle) { console.warn('⚠️ No active cycle found! Using latest cycle as fallback:', { id: latestCycle.id, startDate: latestCycle.startDate, status: latestCycle.status, }); } return latestCycle; }, [staticDataStore.cycles]); // --- D-1 Filter: Keep effective date in sync with WIB day and cutoff --- useEffect(() => { const syncEffectiveDate = () => { const shouldShowPreviousDay = shouldUsePreviousDay(); const nextEffectiveDate = getEffectiveDate(); setIsShowingPreviousDayData(shouldShowPreviousDay); setEffectiveDate((currentEffectiveDate) => { if (formatDateForDb(currentEffectiveDate) === formatDateForDb(nextEffectiveDate)) { return currentEffectiveDate; } return nextEffectiveDate; }); }; // Initialize on mount and keep the date aligned every minute. syncEffectiveDate(); const interval = setInterval(() => { syncEffectiveDate(); }, 60000); // Check every minute return () => clearInterval(interval); }, []); // --- Effect to fetch data on initial load --- useEffect(() => { const fetchData = async () => { setIsLoadingData(true); // Clear alerts and mock schedules (no real-time systems implemented yet) staticDataStore.setAlerts([]); staticDataStore.setWeightAlerts([]); staticDataStore.setFeedingEvents([]); staticDataStore.setVetVisits([]); staticDataStore.setLiveCounts([]); try { // 1. Load kandangs FIRST (needed to get cage UUID for weight data) if (!activeCycle) { console.warn('No active cycle found, skipping data fetch'); setIsLoadingData(false); return; } const kandangsData = await apiClient.getKandangsByCycle(activeCycle.id); setKandangs(kandangsData); // 2. Fetch feed sack data (needed for saldo calculation in KPIs) let feedSackEntries: FeedSackLogEntry[] = []; try { const feedSackResult = await apiClient.getFeedSackCounting(1, undefined, 'LIVE'); feedSackEntries = feedSackResult.entries; staticDataStore.setFeedSackHistory(feedSackEntries); staticDataStore.setAvailableFeedSackCameras(feedSackResult.availableCameras); setFeedSackDataLastUpdate(new Date()); } catch (error) { console.error('❌ Feed sack API error:', error); staticDataStore.setFeedSackHistory([]); staticDataStore.setAvailableFeedSackCameras([]); } // 3. Fetch weight data (using cage UUID from selected kandang) // Get cage UUID from the kandangsData we just loaded // Use current selectedKandangId, or fall back to first kandang if none selected const effectiveKandangId = selectedKandangId !== null ? selectedKandangId : kandangsData[0]?.id; const selectedKandang = kandangsData.find((k: any) => k.id === effectiveKandangId); const cageUuid = selectedKandang?.cageUuid || null; const apiKpiData = await apiClient.getMultiCoopWeightData('LIVE', cageUuid); // If no cage UUID or no data, set empty states if (!apiKpiData) { console.warn('No weight data available (cage UUID not configured or API error)'); staticDataStore.setMultiCoopWeightData({ target: [], coops: [] }); staticDataStore.setFcrComparisonData([]); staticDataStore.setCkaleData([]); staticDataStore.setWeightHistory([]); // KPIs will be set to N/A below } else { const targetHistory: { day: number; weight: number }[] = []; const manualHistory: { day: number; weight: number }[] = []; const iotHistory: { day: number; weight: number }[] = []; // --- Build histories from API KPI data --- for (const dayStr in apiKpiData) { const dayIndex = parseInt(dayStr, 10); const day = dayIndex + 1; // convert 0-based API index to 1-based day-of-cycle const dayData = apiKpiData[dayStr as keyof typeof apiKpiData]; if (dayData.ref_weight > 0) { targetHistory.push({ day, weight: Math.round(dayData.ref_weight) }); } if (dayData.manual_weight > 0) { manualHistory.push({ day, weight: Math.round(dayData.manual_weight) }); } if (dayData.iot_weight > 0) { iotHistory.push({ day, weight: Math.round(dayData.iot_weight) }); } } // --- Update multi-coop comparison page data --- staticDataStore.setMultiCoopWeightData({ target: targetHistory, coops: [ { coopId: 'coop-manual', coopName: 'Manual (Sukawarna)', history: manualHistory, }, { coopId: 'coop-iot', coopName: 'Otomatis (Sukawarna)', history: iotHistory, }, ], }); // --- Build FCR comparison data --- const manualFcrHistory: { day: number; fcr: number | null }[] = []; const iotFcrHistory: { day: number; fcr: number | null }[] = []; const cobbFcrHistory: { day: number; fcr: number | null }[] = []; for (const dayStr in apiKpiData) { const dayIndex = parseInt(dayStr, 10); const day = dayIndex + 1; // convert 0-based API index to 1-based day const dayData = apiKpiData[dayStr as keyof typeof apiKpiData]; // Extract manual FCR (exclude zero values as they indicate system failure) if ( dayData.manual_fcr !== undefined && dayData.manual_fcr !== null && dayData.manual_fcr > 0 ) { manualFcrHistory.push({ day, fcr: dayData.manual_fcr }); } else { manualFcrHistory.push({ day, fcr: null }); } // Extract IoT FCR (exclude zero values as they indicate system failure) if (dayData.iot_fcr !== undefined && dayData.iot_fcr !== null && dayData.iot_fcr > 0) { iotFcrHistory.push({ day, fcr: dayData.iot_fcr }); } else { iotFcrHistory.push({ day, fcr: null }); } // Get COBB standard FCR for this day const cobbStandard = cobbStandardPerformance.find((std) => std.day === day); if (cobbStandard) { cobbFcrHistory.push({ day, fcr: cobbStandard.fcr }); } else { cobbFcrHistory.push({ day, fcr: null }); } } // Combine into single data structure for chart const fcrComparisonData = []; const maxDay = Math.max( manualFcrHistory.length, iotFcrHistory.length, cobbFcrHistory.length ); for (let i = 0; i < maxDay; i++) { const day = i + 1; fcrComparisonData.push({ day, manualFcr: manualFcrHistory[i]?.fcr ?? null, iotFcr: iotFcrHistory[i]?.fcr ?? null, cobbFcr: cobbFcrHistory[i]?.fcr ?? null, }); } // Update FCR comparison data in store staticDataStore.setFcrComparisonData(fcrComparisonData); // --- Build CKALE data for Combined Weight Trend Chart --- // This combines manual and IoT weight data from the API const ckaleDataPoints: CkaleWeightPoint[] = []; for (const dayStr in apiKpiData) { const dayIndex = parseInt(dayStr, 10); // Skip day 0 (DOC weight) - start from day 1 if (dayIndex === 0) continue; const day = dayIndex + 1; // convert 0-based API index to 1-based day const dayData = apiKpiData[dayStr as keyof typeof apiKpiData]; ckaleDataPoints.push({ day, manual: dayData.manual_weight > 0 ? Math.round(dayData.manual_weight) : null, all: dayData.iot_weight > 0 ? Math.round(dayData.iot_weight) : null, front: null, // Not available from API back: null, // Not available from API }); } staticDataStore.setCkaleData(ckaleDataPoints); // --- Also feed Dashboard Berat (weight monitoring) from the same API --- // Prefer IoT weights for dashboard; fall back to manual if IoT not available. const primaryHistorySource = iotHistory.length > 0 ? iotHistory : manualHistory; if (primaryHistorySource.length > 0) { // Sort by day just in case const sortedPrimary = [...primaryHistorySource].sort((a, b) => a.day - b.day); // Map to WeightDataPoint[] used by Dashboard Berat trend chart const weightHistoryData = sortedPrimary.map((point) => { const apiEntry = Object.entries(apiKpiData) .map(([key, value]) => ({ key: parseInt(key, 10) + 1, value })) .find((entry) => entry.key === point.day)?.value as | { ref_weight: number } | undefined; return { day: point.day, actual: point.weight, target: apiEntry ? Math.round(apiEntry.ref_weight) : null, }; }); staticDataStore.setWeightHistory(weightHistoryData); } } // End of apiKpiData check // --- Mortality data will come from the KPI API (kandang-specific) --- // --- Extract Dashboard KPIs from API data --- const { selectedDayData, sortedKeys } = resolveDashboardKpiSelection( apiKpiData, new Date(activeCycle.startDate), effectiveDate ); const formatSparklineDate = (date: Date): string => date .toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'Asia/Jakarta', }) .replace(',', ''); const buildHistory = ( selector: (row: DashboardKpiRow) => number | undefined, includeZeroValues = false ): { values: number[]; dates: string[] } => { const history: number[] = []; const dates: string[] = []; for (const dayIndex of sortedKeys) { const row = apiKpiData[dayIndex.toString()]; const value = selector(row); if (typeof value === 'number' && (includeZeroValues ? value >= 0 : value > 0)) { history.push(value); const pointDate = new Date(activeCycle.startDate); pointDate.setDate(pointDate.getDate() + dayIndex); dates.push(formatSparklineDate(pointDate)); } } return { values: history, dates }; }; const mortalityHistory = buildHistory((row) => row.mortality, true); const weightHistoryValues = buildHistory((row) => row.iot_weight || row.manual_weight); const fcrHistoryValues = buildHistory((row) => row.iot_fcr || row.manual_fcr); const eefHistory = buildHistory((row) => row.iot_eef || row.manual_eef); const foodHistoryValues = buildHistory((row) => row.food, true); // Determine statuses once we know the selected KPI row const getWeightStatus = ( actualWeight: number, refWeight: number ): 'ok' | 'warning' | 'critical' => { const deviation = actualWeight - refWeight; const deviationPercent = (deviation / refWeight) * 100; if (Math.abs(deviationPercent) <= 5) return 'ok'; if (Math.abs(deviationPercent) <= 10) return 'warning'; return 'critical'; }; const getFcrStatus = (fcr: number): 'ok' | 'warning' | 'critical' => { if (fcr <= 1.6) return 'ok'; if (fcr <= 1.8) return 'warning'; return 'critical'; }; const getEefStatus = (eef: number): 'ok' | 'warning' | 'critical' => { if (eef >= 350) return 'ok'; if (eef >= 300) return 'warning'; return 'critical'; }; const getMortalityStatus = (count: number): 'ok' | 'warning' | 'critical' => { if (count < 10) return 'ok'; if (count <= 30) return 'warning'; return 'critical'; }; const umurValue = getLatestApiDayAge(sortedKeys); const latestWeight = selectedDayData?.iot_weight ?? selectedDayData?.manual_weight ?? 0; const latestMortalityCount = selectedDayData?.mortality ?? 0; const fcrValue = selectedDayData?.iot_fcr ?? selectedDayData?.manual_fcr ?? 0; const eefValue = selectedDayData?.iot_eef ?? selectedDayData?.manual_eef ?? 0; const karungDituangToday = selectedDayData?.food ?? 0; const newKpis = { umur: { title: 'Umur', value: umurValue, unit: 'hari', icon: '🐣', trend: [], status: 'ok' as const, }, mortality: { title: 'Mortalitas', value: latestMortalityCount, unit: latestMortalityCount > 0 ? 'ekor' : 'N/A', icon: '⚠️', trend: mortalityHistory.values, trendDates: mortalityHistory.dates, status: latestMortalityCount > 0 ? getMortalityStatus(latestMortalityCount) : ('ok' as const), targetView: 'dataManagement' as const, }, weight: { title: 'BW', value: latestWeight > 0 ? latestWeight : 0, unit: latestWeight > 0 ? 'gram' : 'N/A', icon: '⚖️', trend: weightHistoryValues.values, trendDates: weightHistoryValues.dates, status: latestWeight > 0 && selectedDayData?.ref_weight ? getWeightStatus(latestWeight, selectedDayData.ref_weight) : ('ok' as const), targetView: 'weightMonitoring' as const, }, fcr: { title: 'FCR', value: fcrValue > 0 ? fcrValue : 0, unit: fcrValue > 0 ? '' : 'N/A', icon: '📊', trend: fcrHistoryValues.values, trendDates: fcrHistoryValues.dates, status: fcrValue > 0 ? getFcrStatus(fcrValue) : ('ok' as const), targetView: 'fcrDashboard' as const, }, eef: { title: 'EEF', value: eefValue > 0 ? eefValue : 0, unit: eefValue > 0 ? '' : 'N/A', icon: '📈', trend: eefHistory.values, trendDates: eefHistory.dates, status: eefValue > 0 ? getEefStatus(eefValue) : ('ok' as const), targetView: 'eefDashboard' as const, }, karungDituang: { title: 'Karung Dituang', value: karungDituangToday, unit: 'karung', icon: '🛢️', trend: foodHistoryValues.values, trendDates: foodHistoryValues.dates, status: 'ok' as const, targetView: 'feedSackCounting' as const, }, }; staticDataStore.setKpis(newKpis); } catch (error) { console.error('❌ Error processing multi-coop weight data:', error); console.error('❌ Error stack:', error instanceof Error ? error.stack : error); staticDataStore.setMultiCoopWeightData({ target: [], coops: [] }); // Show N/A for all KPIs on error with neutral status staticDataStore.setKpis({ umur: { title: 'Umur', value: 0, unit: 'N/A', icon: '🐣', trend: [], status: 'ok' as const, }, mortality: { title: 'Mortalitas', value: 0, unit: 'N/A', icon: '⚠️', trend: [], status: 'ok' as const, }, weight: { title: 'BW', value: 0, unit: 'N/A', icon: '⚖️', trend: [], status: 'ok' as const, }, fcr: { title: 'FCR', value: 0, unit: 'N/A', icon: '📊', trend: [], status: 'ok' as const, targetView: 'fcrDashboard' as const, }, eef: { title: 'EEF', value: 0, unit: 'N/A', icon: '📈', trend: [], status: 'ok' as const, targetView: 'eefDashboard' as const, }, karungDituang: { title: 'Karung Dituang', value: 0, unit: 'N/A', icon: '🛢️', trend: [], status: 'ok' as const, }, }); } // 3. Fetch location data try { const locationData = await apiClient.getCageSites('LIVE'); locationStore.initializeFromApi(locationData); } catch (error) { console.error('Cage sites API error:', error); } // 4. Fetch IoT sensor data for the current day using range API try { // Get current date in WIB timezone (API interprets dates in Jakarta timezone) const wibDateStr = new Date() .toLocaleString('en-CA', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit', }) .split(',')[0]; // Format: YYYY-MM-DD // Query current day in WIB timezone const iotRangeData = await apiClient.getIotSensorRange(wibDateStr, wibDateStr, 'LIVE'); // Replace environment data with API data staticDataStore.setEnvironmentData(iotRangeData); } catch (error) { console.error('IoT sensor range fetch error:', error); } }; fetchData().finally(() => { // Set loading to false after all API calls complete setTimeout(() => setIsLoadingData(false), 500); // Small delay for smooth transition }); }, [ staticDataStore.setMultiCoopWeightData, staticDataStore.setInitialWeight, staticDataStore.setWeightHistory, staticDataStore.setFeedSackHistory, staticDataStore.setEnvironmentData, locationStore.initializeFromApi, effectiveDate, ]); // --- IoT Polling Effect (60 seconds) --- // Refetch range data periodically to keep the graph updated with latest readings useEffect(() => { const fetchIotRangeData = async () => { try { // Get current date in WIB timezone (API interprets dates in Jakarta timezone) const wibDateStr = new Date() .toLocaleString('en-CA', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit', }) .split(',')[0]; // Format: YYYY-MM-DD // Query current day in WIB timezone const iotRangeData = await apiClient.getIotSensorRange(wibDateStr, wibDateStr, 'LIVE'); // Update environment data with fresh API data staticDataStore.setEnvironmentData(iotRangeData); } catch (error) { console.error('IoT polling error:', error); } }; // Poll every 60 seconds to get latest data const pollInterval = setInterval(fetchIotRangeData, 60000); return () => clearInterval(pollInterval); }, [staticDataStore.setEnvironmentData, staticDataStore.setKpis]); // --- Weight Distribution Fetcher --- const fetchWeightDistribution = useCallback(async () => { // Guard: Check if activeCycle and startDate exist if (!activeCycle || !activeCycle.startDate) { console.warn('fetchWeightDistribution: activeCycle or startDate not available'); return; } 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); const cycleStartStr = activeCycle.startDate instanceof Date ? activeCycle.startDate.toISOString().split('T')[0] : activeCycle.startDate; const response: WeightDistributionApiResponse = await apiClient.getWeightDistributionRange( adjustedRange.start, adjustedRange.end, 'LIVE', cycleStartStr ); // Process response if (response.success) { // Get data (could be array with single element or single object) const apiData = Array.isArray(response.data) ? response.data[0] : response.data; // CRITICAL: Handle empty data as per user requirement if (!apiData || !apiData.distribusi || apiData.distribusi.length === 0) { staticDataStore.setWeightDistribution([]); staticDataStore.setDistributionStats(null); return; } // Transform API data to chart format const chartData: WeightDistributionPoint[] = apiData.distribusi.map((item) => ({ range: item.range_bobot, count: item.total_ayam, })); // Calculate day of cycle const dataDate = new Date(apiData.tanggal); const cycleStart = new Date(activeCycle.startDate); dataDate.setHours(0, 0, 0, 0); cycleStart.setHours(0, 0, 0, 0); const dayOfCycle = Math.floor((dataDate.getTime() - cycleStart.getTime()) / (1000 * 60 * 60 * 24)) + 1; // Store statistics const stats: WeightDistributionStats = { date: new Date(apiData.tanggal).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }), dayOfCycle, totalPredictions: apiData.total_predictions, totalChickenCount: apiData.total_chicken_count, min: apiData.statistik.min, max: apiData.statistik.max, avg: apiData.statistik.avg, standardDeviation: apiData.statistik.Standard_deviasi, uniformity: apiData.statistik.uniformity, }; staticDataStore.setWeightDistribution(chartData); staticDataStore.setDistributionStats(stats); } } catch (error) { console.error('Error fetching weight distribution:', error); // Keep existing data on error } }, [staticDataStore.distributionSelectedDate, activeCycle.startDate, staticDataStore]); // --- Sensor Weight Summary Fetcher (also fetches device weight data for selected date) --- 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'); if (response.success) { staticDataStore.setSensorWeightSummary(response.data); // Also fetch cage info and process device weight data for the comparison chart const cageUuid = getCurrentCageUuid(); // Skip cage data if no UUID configured if (cageUuid) { const cageUrl = `https://dashboard.cpsp.id/api/scales/cage/${cageUuid}`; const cageResponse = await fetch(cageUrl); const cageData = await cageResponse.json(); // Process device weight data for the selected date const deviceWeight = processDeviceWeightData(response, cageData.data?.devices || []); staticDataStore.setDeviceWeightData(deviceWeight); } else { console.warn('No cage UUID configured, skipping device weight data'); staticDataStore.setDeviceWeightData([]); } } } catch (error) { console.error('Error fetching sensor weight summary:', error); staticDataStore.setSensorWeightSummary([]); staticDataStore.setDeviceWeightData([]); } finally { staticDataStore.setDailyDataLoading(false); } }, [staticDataStore.distributionSelectedDate, staticDataStore]); // --- Scale Data Fetcher (for revamped weight dashboard) --- const fetchScaleData = useCallback(async () => { if (!activeCycle || !activeCycle.startDate) { console.warn('fetchScaleData: activeCycle or startDate not available'); return; } staticDataStore.setScaleDataLoading(true); try { const cycleStart = activeCycle.startDate instanceof Date ? activeCycle.startDate : new Date(activeCycle.startDate); const cycleStartStr = cycleStart.toISOString().split('T')[0]; const statsDate = new Date(effectiveDate); statsDate.setHours(0, 0, 0, 0); const previousStatsDate = new Date(statsDate); previousStatsDate.setDate(previousStatsDate.getDate() - 1); const dayBeforePreviousStatsDate = new Date(previousStatsDate); dayBeforePreviousStatsDate.setDate(dayBeforePreviousStatsDate.getDate() - 1); const statsDateStr = formatDateForDb(statsDate); const previousStatsDateStr = formatDateForDb(previousStatsDate); const dayBeforePreviousStatsDateStr = formatDateForDb(dayBeforePreviousStatsDate); const displayDate = formatEffectiveDateLabel(statsDate); // Fetch scale predictions range for entire cycle const rangeResponse = await apiClient.getScalePredictionsRange( cycleStartStr, statsDateStr, 'LIVE' ); // Fetch effective day and prior distributions to keep stats aligned with D-1 logic const currentDistribution = await apiClient.getWeightDistributionRange( statsDateStr, statsDateStr, 'LIVE', cycleStartStr ); const previousDistribution = await apiClient.getWeightDistributionRange( previousStatsDateStr, previousStatsDateStr, 'LIVE', cycleStartStr ); const dayBeforePreviousDistribution = await apiClient.getWeightDistributionRange( dayBeforePreviousStatsDateStr, dayBeforePreviousStatsDateStr, 'LIVE', cycleStartStr ); if (rangeResponse.success && currentDistribution.success) { // Process daily weight data const dailyWeight = processDailyWeightData( rangeResponse, cycleStart, activeCycle.totalDays ); staticDataStore.setDailyWeightData(dailyWeight); // Calculate daily weight gain const dailyGain = calculateDailyWeightGain(dailyWeight); staticDataStore.setDailyWeightGainData(dailyGain); // Use the effective date first, then fallback to the previous day if needed let selectedDistData = Array.isArray(currentDistribution.data) ? currentDistribution.data[0] : currentDistribution.data; let selectedDateLabel = displayDate; let comparisonDistData = Array.isArray(previousDistribution.data) ? previousDistribution.data[0] : previousDistribution.data; if (!selectedDistData || selectedDistData.total_chicken_count === 0) { if (previousDistribution.success && comparisonDistData?.total_chicken_count > 0) { selectedDistData = comparisonDistData; selectedDateLabel = formatEffectiveDateLabel(previousStatsDate); comparisonDistData = Array.isArray(dayBeforePreviousDistribution.data) ? dayBeforePreviousDistribution.data[0] : dayBeforePreviousDistribution.data; } } // If still no data available (both today and yesterday empty) if (!selectedDistData || selectedDistData.total_chicken_count === 0) { // Set empty/null stats to indicate no data staticDataStore.setInitialWeight({ averageWeight: { value: 0, unit: 'N/A', status: 'critical', }, avgDailyGain: { value: 0, unit: 'N/A', status: 'critical', }, uniformity: { value: 0, unit: 'N/A', status: 'critical', }, dataDate: displayDate, }); } else { // Get average weight from distribusi bobot API const avgWeight = selectedDistData.statistik.avg || 0; // Calculate daily gain from previous day's distribusi bobot API let dailyGain = 0; const priorAvg = comparisonDistData?.statistik?.avg || 0; if (priorAvg > 0) { dailyGain = avgWeight - priorAvg; } // Get uniformity from distribusi bobot API const uniformity = selectedDistData.statistik.uniformity || 0; // Status helper functions const getAdgStatus = (adg: number): 'ok' | 'warning' | 'critical' => { if (adg >= 60) return 'ok'; if (adg >= 40) return 'warning'; return 'critical'; }; const getUniformityStatus = (uniformity: number): 'ok' | 'warning' | 'critical' => { if (uniformity >= 85) return 'ok'; if (uniformity >= 75) return 'warning'; return 'critical'; }; // Set stats from distribusi bobot API // Keep precision: avgWeight with 2 decimals, uniformity with 1 decimal (no rounding yet for display) const stats = { averageWeight: { value: avgWeight, // Keep full precision, will format in display unit: 'g', status: 'ok' as const, // Will be updated with actual target comparison if needed }, avgDailyGain: { value: Math.round(dailyGain), // Round ADG to whole number unit: 'g/hari', status: getAdgStatus(dailyGain), }, uniformity: { value: uniformity, // Keep full precision, will format in display unit: '%', status: getUniformityStatus(uniformity), }, dataDate: selectedDateLabel, // Add the date label }; staticDataStore.setInitialWeight(stats); } staticDataStore.setScaleDataLastUpdate(new Date()); } } catch (error) { console.error('❌ Error fetching scale data:', error); // Keep existing data on error } finally { staticDataStore.setScaleDataLoading(false); } }, [activeCycle, effectiveDate]); // Re-run when the active cycle or effective date changes // Refresh function (clears cache and refetches) const refreshScaleData = useCallback(async () => { await fetchScaleData(); }, [fetchScaleData]); // --- Auto-refresh scale data every 30 minutes --- useEffect(() => { // Only run if activeCycle is available if (!activeCycle || !activeCycle.startDate) { return; } // Initial fetch fetchScaleData(); // Set up 30-minute interval const interval = setInterval( () => { fetchScaleData(); }, 30 * 60 * 1000 ); // 30 minutes return () => { clearInterval(interval); }; }, [activeCycle, fetchScaleData]); // Re-run when activeCycle or fetchScaleData changes // --- Effect to fetch weight distribution when date changes --- useEffect(() => { // Auto-fetch when selected date changes or when activeCycle becomes available fetchWeightDistribution(); fetchSensorWeightSummary(); }, [staticDataStore.distributionSelectedDate, activeCycle]); // eslint-disable-line react-hooks/exhaustive-deps // --- Effect to reload weight data when kandang selection changes --- useEffect(() => { const abortController = new AbortController(); // Only reload if we have an active cycle and kandang data is available if (activeCycle && kandangs.length > 0 && selectedKandangId !== null) { // Refetch weight KPI data with new cage UUID const refetchWeightKpiData = async () => { setIsRefreshingWeightKpi(true); try { const selectedKandang = kandangs.find((k: any) => k.id === selectedKandangId); const cageUuid = selectedKandang?.cageUuid || null; const apiKpiData = await apiClient.getMultiCoopWeightData('LIVE', cageUuid); if (!apiKpiData) { console.warn('No weight data available (cage UUID not configured or API error)'); staticDataStore.setMultiCoopWeightData({ target: [], coops: [] }); staticDataStore.setFcrComparisonData([]); staticDataStore.setCkaleData([]); staticDataStore.setWeightHistory([]); staticDataStore.setInitialWeight({ averageWeight: { value: 0, unit: 'N/A', status: 'ok' }, avgDailyGain: { value: 0, unit: 'N/A', status: 'ok' }, uniformity: { value: 0, unit: 'N/A', status: 'ok' }, dataDate: formatEffectiveDateLabel(effectiveDate), }); // Update KPIs to show N/A for weight-related metrics staticDataStore.setKpis((currentKpis) => ({ ...currentKpis, umur: { title: 'Umur', value: 0, unit: 'N/A', icon: '🐣', trend: [], status: 'ok', }, mortality: { title: 'Mortalitas', value: 0, unit: 'N/A', icon: '⚠️', trend: [], status: 'ok', targetView: 'dataManagement', }, weight: { title: 'BW', value: 0, unit: 'N/A', icon: '⚖️', trend: [], status: 'ok', targetView: 'weightMonitoring', }, fcr: { title: 'FCR', value: 0, unit: 'N/A', icon: '📊', trend: [], status: 'ok', targetView: 'multiCoopWeight', }, eef: { title: 'EEF', value: 0, unit: 'N/A', icon: '📈', trend: [], status: 'ok', }, karungDituang: { title: 'Karung Dituang', value: 0, unit: 'N/A', icon: '🛢️', trend: [], status: 'ok', targetView: 'feedSackCounting', }, })); } else { // Rebuild weight data stores from API (same logic as initial load) const targetHistory: { day: number; weight: number }[] = []; const manualHistory: { day: number; weight: number }[] = []; const iotHistory: { day: number; weight: number }[] = []; for (const dayStr in apiKpiData) { const dayIndex = parseInt(dayStr, 10); const day = dayIndex + 1; const dayData = apiKpiData[dayStr as keyof typeof apiKpiData]; if (dayData.ref_weight > 0) { targetHistory.push({ day, weight: Math.round(dayData.ref_weight) }); } if (dayData.manual_weight > 0) { manualHistory.push({ day, weight: Math.round(dayData.manual_weight) }); } if (dayData.iot_weight > 0) { iotHistory.push({ day, weight: Math.round(dayData.iot_weight) }); } } staticDataStore.setMultiCoopWeightData({ target: targetHistory, coops: [ { coopId: 'coop-manual', coopName: 'Manual (Sukawarna)', history: manualHistory }, { coopId: 'coop-iot', coopName: 'Otomatis (Sukawarna)', history: iotHistory }, ], }); // Weight KPI cards/history only; weightStats comes from fetchScaleData (distribusi bobot API). const primaryHistorySource = iotHistory.length > 0 ? iotHistory : manualHistory; if (primaryHistorySource.length > 0) { const sortedPrimary = [...primaryHistorySource].sort((a, b) => a.day - b.day); const effectiveDay = getCycleDayNumber(activeCycle.startDate, effectiveDate); const selectedDay = effectiveDay > 0 && sortedPrimary.some((point) => point.day === effectiveDay) ? effectiveDay : sortedPrimary[sortedPrimary.length - 1].day; const last = sortedPrimary.find((point) => point.day === selectedDay) || sortedPrimary[sortedPrimary.length - 1]; if (!last) { console.warn('Insufficient weight data for stats calculation'); return; } const apiEntryForLastDay = Object.entries(apiKpiData) .map(([key, value]) => ({ key: parseInt(key, 10) + 1, value })) .find((entry) => entry.key === last.day)?.value as | { ref_weight: number; manual_weight: number; iot_weight: number } | undefined; const refWeight = apiEntryForLastDay?.ref_weight ?? last.weight; const weightHistoryData = sortedPrimary.map((point) => { const apiEntry = Object.entries(apiKpiData) .map(([key, value]) => ({ key: parseInt(key, 10) + 1, value })) .find((entry) => entry.key === point.day)?.value as | { ref_weight: number } | undefined; return { day: point.day, actual: point.weight, target: apiEntry ? Math.round(apiEntry.ref_weight) : null, }; }); staticDataStore.setWeightHistory(weightHistoryData); // Update the weight-related KPIs without clobbering feed-sack values const getWeightStatus = ( actualWeight: number, refWeight: number ): 'ok' | 'warning' | 'critical' => { const deviation = actualWeight - refWeight; const deviationPercent = (deviation / refWeight) * 100; if (Math.abs(deviationPercent) <= 5) return 'ok'; if (Math.abs(deviationPercent) <= 10) return 'warning'; return 'critical'; }; const getFcrStatus = (fcr: number): 'ok' | 'warning' | 'critical' => { if (fcr <= 1.6) return 'ok'; if (fcr <= 1.8) return 'warning'; return 'critical'; }; const getEefStatus = (eef: number): 'ok' | 'warning' | 'critical' => { if (eef >= 350) return 'ok'; if (eef >= 300) return 'warning'; return 'critical'; }; const { selectedDayData } = resolveDashboardKpiSelection( apiKpiData, new Date(activeCycle.startDate), effectiveDate ); // Build FCR history const fcrHistory: number[] = []; const sortedKeys = Object.keys(apiKpiData) .map((k) => parseInt(k, 10)) .sort((a, b) => a - b); const foodHistory: number[] = []; for (const dayIndex of sortedKeys) { const dayData = apiKpiData[dayIndex.toString()]; foodHistory.push(dayData.food || 0); const fcrValue = dayData.iot_fcr || dayData.manual_fcr; if (fcrValue > 0) { fcrHistory.push(fcrValue); } } // Build EEF history const eefHistory: number[] = []; for (const dayIndex of sortedKeys) { const dayData = apiKpiData[dayIndex.toString()]; const eefValue = dayData.iot_eef || dayData.manual_eef; if (eefValue > 0) { eefHistory.push(eefValue); } } const selectedWeight = selectedDayData?.iot_weight ?? selectedDayData?.manual_weight ?? last.weight; const selectedFcr = selectedDayData?.iot_fcr ?? selectedDayData?.manual_fcr ?? 0; const selectedEef = selectedDayData?.iot_eef ?? selectedDayData?.manual_eef ?? 0; const selectedKarung = selectedDayData?.food ?? 0; const latestApiDayAge = getLatestApiDayAge(sortedKeys); staticDataStore.setKpis((currentKpis) => ({ ...currentKpis, umur: { title: 'Umur', value: latestApiDayAge, unit: 'hari', icon: '🐣', trend: [], status: 'ok', }, weight: { title: 'BW', value: selectedWeight, unit: 'gram', icon: '⚖️', trend: sortedPrimary.map((p) => p.weight), status: getWeightStatus(selectedWeight, selectedDayData?.ref_weight ?? refWeight), targetView: 'weightMonitoring', }, fcr: { title: 'FCR', value: selectedFcr > 0 ? selectedFcr : 0, unit: selectedFcr > 0 ? '' : 'N/A', icon: '📊', trend: fcrHistory, status: selectedFcr > 0 ? getFcrStatus(selectedFcr) : 'critical', targetView: 'multiCoopWeight', }, eef: { title: 'EEF', value: selectedEef > 0 ? selectedEef : 0, unit: selectedEef > 0 ? '' : 'N/A', icon: '📈', trend: eefHistory, status: selectedEef > 0 ? getEefStatus(selectedEef) : 'critical', }, karungDituang: { title: 'Karung Dituang', value: selectedKarung, unit: 'karung', icon: '🛢️', trend: foodHistory, status: 'ok', targetView: 'feedSackCounting', }, })); } } } catch (error) { console.error('Error refetching weight KPI data:', error); } finally { setIsRefreshingWeightKpi(false); } }; // Run refetch and wait for it to complete before clearing loading state refetchWeightKpiData().then(() => { // Also reload weight-related data for other pages (don't wait for these) fetchScaleData(); fetchSensorWeightSummary(); }); } return () => { abortController.abort(); }; }, [selectedKandangId, kandangs.length]); // eslint-disable-line react-hooks/exhaustive-deps // --- Permission Checker --- const hasPermission = useCallback( (permissionId: string): boolean => { if (!currentUser) return false; if (currentUser.roleId === 'role-1') return true; // Admin has all permissions return accessManagementStore.rolePermissions.some( (rp) => rp.roleId === currentUser.roleId && rp.permissionId === permissionId ); }, [currentUser, accessManagementStore.rolePermissions] ); // --- Authentication Handlers --- const login = useCallback( (user: User) => { const adminUser = accessManagementStore.users.find((u) => u.roleId === 'role-1'); if (adminUser) { setCurrentUser(adminUser); } else { setCurrentUser(user); } }, [accessManagementStore.users] ); const logout = useCallback(() => setCurrentUser(null), []); // --- Feed Sack Data Fetcher (on-demand with caching) --- const fetchFeedSackData = useCallback( async (forceRefresh = false) => { try { // Check cache validity (30 minutes = 1800000 ms) const now = new Date(); const cacheAge = feedSackDataLastUpdate ? now.getTime() - feedSackDataLastUpdate.getTime() : Infinity; const isCacheValid = cacheAge < 30 * 60 * 1000; // 30 minutes // Use cache if valid and not forcing refresh if (!forceRefresh && isCacheValid && staticDataStore.feedSackHistory.length > 0) { return; } setIsFeedSackDataLoading(true); const result = await apiClient.getFeedSackCounting(1, undefined, 'LIVE'); staticDataStore.setFeedSackHistory(result.entries); staticDataStore.setAvailableFeedSackCameras(result.availableCameras); setFeedSackDataLastUpdate(new Date()); } catch (error) { console.error('❌ Feed sack API error:', error); staticDataStore.setFeedSackHistory([]); staticDataStore.setAvailableFeedSackCameras([]); } finally { setIsFeedSackDataLoading(false); } }, [ feedSackDataLastUpdate, staticDataStore.setFeedSackHistory, staticDataStore.setAvailableFeedSackCameras, staticDataStore.feedSackHistory.length, setIsFeedSackDataLoading, ] ); // --- Manual Refresh for Feed Sack Data --- const refreshFeedSackData = useCallback(async () => { await fetchFeedSackData(true); // Force refresh }, [fetchFeedSackData]); // --- Assemble final context value --- const value: AppContextType = { currentUser, login, logout, hasPermission, isLoadingData, isRefreshingWeightKpi, ...accessManagementStore, ...locationStore, ...deviceStore, ...staticDataStore, ...crossStoreHandlers, ...derivedState, ...locationHelpers, cycleData: activeCycle, // Explicitly include device add/update from deviceStore to satisfy the type onAddCamera: deviceStore.onAddCamera, onUpdateCamera: deviceStore.onUpdateCamera, onDeleteCamera: deviceStore.onDeleteCamera, onAddIotDevice: deviceStore.onAddIotDevice, onUpdateIotDevice: deviceStore.onUpdateIotDevice, onDeleteIotDevice: deviceStore.onDeleteIotDevice, // Explicitly include location add/update from locationStore onAddLocation: locationStore.onAddLocation, onUpdateLocation: locationStore.onUpdateLocation, onAddCoop: locationStore.onAddCoop, onUpdateCoop: locationStore.onUpdateCoop, onAddFloor: locationStore.onAddFloor, onUpdateFloor: locationStore.onUpdateFloor, // Weight stats rename weightStats: staticDataStore.initialWeight, // Weight distribution date selection distributionSelectedDate: staticDataStore.distributionSelectedDate, distributionStats: staticDataStore.distributionStats, setDistributionSelectedDate: staticDataStore.setDistributionSelectedDate, fetchWeightDistribution, fetchFeedSackData, fetchManualFeedSackData: staticDataStore.fetchManualFeedSackData, // Feed sack cache state and refresh feedSackDataLastUpdate, isFeedSackDataLoading, refreshFeedSackData, // New scale data for revamped dashboard dailyWeightData: staticDataStore.dailyWeightData, dailyWeightGainData: staticDataStore.dailyWeightGainData, deviceWeightData: staticDataStore.deviceWeightData, scaleDataLoading: staticDataStore.scaleDataLoading, scaleDataLastUpdate: staticDataStore.scaleDataLastUpdate, dailyDataLoading: staticDataStore.dailyDataLoading, fetchScaleData, refreshScaleData, // API mode - always LIVE since we're using real APIs apiMode: 'LIVE', // D-1 filter state effectiveDate, isShowingPreviousDayData, }; return {children}; }; export const useAppContext = (): AppContextType => { const context = useContext(AppContext); if (context === undefined) { throw new Error('useAppContext must be used within an AppContextProvider'); } return context; };