commit
32a36cceff
444 files changed
+67186
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocationStore } from '../stores/useLocationStore.ts';
|
||||
import { useDeviceStore } from '../stores/useDeviceStore.ts';
|
||||
|
||||
export const useCrossStoreHandlers = (
|
||||
locationStore: ReturnType<typeof useLocationStore>,
|
||||
deviceStore: ReturnType<typeof useDeviceStore>
|
||||
) => {
|
||||
const onDeleteFloor = useCallback(
|
||||
(floorId: string) => {
|
||||
locationStore.onDeleteFloor(floorId);
|
||||
deviceStore.unassignDevicesFromFloor(floorId);
|
||||
},
|
||||
[locationStore, deviceStore]
|
||||
);
|
||||
|
||||
const onDeleteCoop = useCallback(
|
||||
(coopId: string) => {
|
||||
const floorsToDelete = locationStore.floors.filter((f) => f.coopId === coopId);
|
||||
floorsToDelete.forEach((floor) => onDeleteFloor(floor.id));
|
||||
locationStore.onDeleteCoop(coopId);
|
||||
},
|
||||
[locationStore, onDeleteFloor]
|
||||
);
|
||||
|
||||
const onDeleteLocation = useCallback(
|
||||
(locationId: string) => {
|
||||
const coopsToDelete = locationStore.coops.filter((c) => c.locationId === locationId);
|
||||
coopsToDelete.forEach((coop) => onDeleteCoop(coop.id));
|
||||
locationStore.onDeleteLocation(locationId);
|
||||
},
|
||||
[locationStore, onDeleteCoop]
|
||||
);
|
||||
|
||||
return {
|
||||
onDeleteFloor,
|
||||
onDeleteCoop,
|
||||
onDeleteLocation,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDeviceStore } from '../stores/useDeviceStore.ts';
|
||||
import { useStaticDataStore } from '../stores/useStaticDataStore.ts';
|
||||
import { useLocationStore } from '../stores/useLocationStore.ts';
|
||||
import { AggregatedCountLocation, LiveCount, NetworkDevice } from '../../types.ts';
|
||||
|
||||
export const useDerivedState = (
|
||||
deviceStore: ReturnType<typeof useDeviceStore>,
|
||||
staticDataStore: ReturnType<typeof useStaticDataStore>,
|
||||
locationStore: ReturnType<typeof useLocationStore>
|
||||
) => {
|
||||
const { cameras } = deviceStore;
|
||||
const { liveCounts } = staticDataStore;
|
||||
const { locations, coops, floors } = locationStore;
|
||||
|
||||
const aggregatedCounts = useMemo<AggregatedCountLocation[]>(() => {
|
||||
const cameraMap = new Map<string, LiveCount>();
|
||||
liveCounts.forEach((lc) => cameraMap.set(lc.cameraId, lc));
|
||||
|
||||
const floorMap = new Map<
|
||||
string,
|
||||
{ floorName: string; coopId: string; count: number; cameras: LiveCount[] }
|
||||
>();
|
||||
cameras.forEach((camera) => {
|
||||
const liveCount = cameraMap.get(camera.id);
|
||||
if (camera.floorId && liveCount && liveCount.status !== 'offline') {
|
||||
if (!floorMap.has(camera.floorId)) {
|
||||
const floor = floors.find((f) => f.id === camera.floorId);
|
||||
floorMap.set(camera.floorId, {
|
||||
floorName: floor?.name || 'Unknown Floor',
|
||||
coopId: floor?.coopId || 'unknown-coop',
|
||||
count: 0,
|
||||
cameras: [],
|
||||
});
|
||||
}
|
||||
const floorData = floorMap.get(camera.floorId)!;
|
||||
floorData.count += liveCount.count;
|
||||
floorData.cameras.push(liveCount);
|
||||
}
|
||||
});
|
||||
|
||||
const coopMap = new Map<
|
||||
string,
|
||||
{ coopName: string; locationId: string; totalCount: number; floors: any[] }
|
||||
>();
|
||||
floorMap.forEach((data, floorId) => {
|
||||
if (!coopMap.has(data.coopId)) {
|
||||
const coop = coops.find((c) => c.id === data.coopId);
|
||||
coopMap.set(data.coopId, {
|
||||
coopName: coop?.name || 'Unknown Coop',
|
||||
locationId: coop?.locationId || 'unknown-loc',
|
||||
totalCount: 0,
|
||||
floors: [],
|
||||
});
|
||||
}
|
||||
const coopData = coopMap.get(data.coopId)!;
|
||||
coopData.totalCount += data.count;
|
||||
coopData.floors.push({
|
||||
floorId,
|
||||
floorName: data.floorName,
|
||||
count: data.count,
|
||||
cameras: data.cameras,
|
||||
});
|
||||
});
|
||||
|
||||
const locationMap = new Map<
|
||||
string,
|
||||
{ locationName: string; totalCount: number; coops: any[] }
|
||||
>();
|
||||
coopMap.forEach((data, coopId) => {
|
||||
if (!locationMap.has(data.locationId)) {
|
||||
const loc = locations.find((l) => l.id === data.locationId);
|
||||
locationMap.set(data.locationId, {
|
||||
locationName: loc?.name || 'Unknown Location',
|
||||
totalCount: 0,
|
||||
coops: [],
|
||||
});
|
||||
}
|
||||
const locData = locationMap.get(data.locationId)!;
|
||||
locData.totalCount += data.totalCount;
|
||||
locData.coops.push({
|
||||
coopId,
|
||||
coopName: data.coopName,
|
||||
totalCount: data.totalCount,
|
||||
floors: data.floors,
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(locationMap.entries()).map(([locationId, data]) => ({ locationId, ...data }));
|
||||
}, [liveCounts, cameras, locations, coops, floors]);
|
||||
|
||||
const getNetworkDeviceLocationName = (device: NetworkDevice): string => {
|
||||
if (!device.locationId) return 'Global';
|
||||
|
||||
const floor = floors.find((f) => f.id === device.locationId);
|
||||
if (floor) {
|
||||
const coop = coops.find((c) => c.id === floor.coopId);
|
||||
const location = locations.find((l) => l.id === coop?.locationId);
|
||||
return `${location?.name || ''} / ${coop?.name || ''} / ${floor.name}`;
|
||||
}
|
||||
|
||||
const coop = coops.find((c) => c.id === device.locationId);
|
||||
if (coop) {
|
||||
const location = locations.find((l) => l.id === coop.locationId);
|
||||
return `${location?.name || ''} / ${coop.name}`;
|
||||
}
|
||||
|
||||
const location = locations.find((l) => l.id === device.locationId);
|
||||
if (location) return location.name;
|
||||
|
||||
return 'Lokasi Tidak Dikenal';
|
||||
};
|
||||
|
||||
return {
|
||||
aggregatedCounts,
|
||||
getNetworkDeviceLocationName,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocationStore } from '../stores/useLocationStore.ts';
|
||||
|
||||
export const useLocationHelpers = (locationStore: ReturnType<typeof useLocationStore>) => {
|
||||
const { locations, coops, floors } = locationStore;
|
||||
|
||||
const getFullLocationName = useCallback(
|
||||
(floorId: string | null): string => {
|
||||
if (!floorId) return 'Belum Ditempatkan';
|
||||
const floor = floors.find((f) => f.id === floorId);
|
||||
if (!floor) return 'Lantai Tidak Dikenal';
|
||||
|
||||
const coop = coops.find((c) => c.id === floor.coopId);
|
||||
if (!coop) return 'Kandang Tidak Dikenal';
|
||||
|
||||
const location = locations.find((l) => l.id === coop.locationId);
|
||||
if (!location) return 'Lokasi Tidak Dikenal';
|
||||
|
||||
return `${location.name} / ${coop.name} / ${floor.name}`;
|
||||
},
|
||||
[locations, coops, floors]
|
||||
);
|
||||
|
||||
return {
|
||||
getFullLocationName,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { User, Group, Role, Permission, RolePermission } from '../../types.ts';
|
||||
import {
|
||||
initialUsers,
|
||||
initialGroups,
|
||||
initialRoles,
|
||||
allPermissions,
|
||||
initialRolePermissions,
|
||||
} from '../../mockData/accessManagementData.ts';
|
||||
|
||||
export const useAccessManagementStore = () => {
|
||||
const [users, setUsers] = useState<User[]>(initialUsers);
|
||||
const [groups, _setGroups] = useState<Group[]>(initialGroups);
|
||||
const [roles, _setRoles] = useState<Role[]>(initialRoles);
|
||||
const [permissions, _setPermissions] = useState<Permission[]>(allPermissions);
|
||||
const [rolePermissions, setRolePermissions] = useState<RolePermission[]>(initialRolePermissions);
|
||||
|
||||
const onAddUser = useCallback(
|
||||
(userData: Omit<User, 'id'>) =>
|
||||
setUsers((prev) => [...prev, { ...userData, id: `user-${Date.now()}` }]),
|
||||
[]
|
||||
);
|
||||
const onUpdateUser = useCallback(
|
||||
(updatedUser: User) =>
|
||||
setUsers((prev) => prev.map((u) => (u.id === updatedUser.id ? updatedUser : u))),
|
||||
[]
|
||||
);
|
||||
const onDeleteUser = useCallback(
|
||||
(userId: string) => setUsers((prev) => prev.filter((u) => u.id !== userId)),
|
||||
[]
|
||||
);
|
||||
|
||||
const onTogglePermission = useCallback((roleId: string, permissionId: string) => {
|
||||
setRolePermissions((prev) => {
|
||||
const hasPermission = prev.some(
|
||||
(rp) => rp.roleId === roleId && rp.permissionId === permissionId
|
||||
);
|
||||
if (hasPermission) {
|
||||
return prev.filter((rp) => !(rp.roleId === roleId && rp.permissionId === permissionId));
|
||||
} else {
|
||||
return [...prev, { roleId, permissionId }];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
users,
|
||||
groups,
|
||||
roles,
|
||||
permissions,
|
||||
rolePermissions,
|
||||
onAddUser,
|
||||
onUpdateUser,
|
||||
onDeleteUser,
|
||||
onTogglePermission,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Camera, IotDevice } from '../../types.ts';
|
||||
import { initialCameras } from '../../mockData/cameraData.ts';
|
||||
import { initialIotDevices } from '../../mockData/iotDeviceData.ts';
|
||||
|
||||
export const useDeviceStore = () => {
|
||||
const [cameras, setCameras] = useState<Camera[]>(initialCameras);
|
||||
const [iotDevices, setIotDevices] = useState<IotDevice[]>(initialIotDevices);
|
||||
|
||||
const onAddCamera = useCallback(
|
||||
(cameraData: Omit<Camera, 'id' | 'status'>) =>
|
||||
setCameras((prev) => [...prev, { ...cameraData, id: `cam${Date.now()}`, status: 'online' }]),
|
||||
[]
|
||||
);
|
||||
const onUpdateCamera = useCallback(
|
||||
(updatedCamera: Camera) =>
|
||||
setCameras((prev) => prev.map((c) => (c.id === updatedCamera.id ? updatedCamera : c))),
|
||||
[]
|
||||
);
|
||||
const onDeleteCamera = useCallback(
|
||||
(cameraId: string) => setCameras((prev) => prev.filter((c) => c.id !== cameraId)),
|
||||
[]
|
||||
);
|
||||
|
||||
const onAddIotDevice = useCallback(
|
||||
(deviceData: Omit<IotDevice, 'id' | 'status' | 'lastHeartbeat'>) =>
|
||||
setIotDevices((prev) => [
|
||||
...prev,
|
||||
{ ...deviceData, id: `iot-${Date.now()}`, status: 'online', lastHeartbeat: new Date() },
|
||||
]),
|
||||
[]
|
||||
);
|
||||
const onUpdateIotDevice = useCallback(
|
||||
(updatedDevice: IotDevice) =>
|
||||
setIotDevices((prev) => prev.map((d) => (d.id === updatedDevice.id ? updatedDevice : d))),
|
||||
[]
|
||||
);
|
||||
const onDeleteIotDevice = useCallback(
|
||||
(deviceId: string) => setIotDevices((prev) => prev.filter((d) => d.id !== deviceId)),
|
||||
[]
|
||||
);
|
||||
|
||||
const unassignDevicesFromFloor = useCallback((floorId: string) => {
|
||||
setCameras((prev) =>
|
||||
prev.map((cam) => (cam.floorId === floorId ? { ...cam, floorId: null } : cam))
|
||||
);
|
||||
setIotDevices((prev) =>
|
||||
prev.map((dev) => (dev.floorId === floorId ? { ...dev, floorId: null } : dev))
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
cameras,
|
||||
iotDevices,
|
||||
onAddCamera,
|
||||
onUpdateCamera,
|
||||
onDeleteCamera,
|
||||
onAddIotDevice,
|
||||
onUpdateIotDevice,
|
||||
onDeleteIotDevice,
|
||||
unassignDevicesFromFloor,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Location, Coop, Floor } from '../../types.ts';
|
||||
import { initialLocations, initialCoops, initialFloors } from '../../mockData/locationData.ts';
|
||||
|
||||
export const useLocationStore = () => {
|
||||
const [locations, setLocations] = useState<Location[]>(initialLocations);
|
||||
const [coops, setCoops] = useState<Coop[]>(initialCoops);
|
||||
const [floors, setFloors] = useState<Floor[]>(initialFloors);
|
||||
|
||||
const onAddLocation = useCallback(
|
||||
(name: string) => setLocations((prev) => [...prev, { id: `loc${Date.now()}`, name }]),
|
||||
[]
|
||||
);
|
||||
const onUpdateLocation = useCallback(
|
||||
(updatedLocation: Location) =>
|
||||
setLocations((prev) => prev.map((l) => (l.id === updatedLocation.id ? updatedLocation : l))),
|
||||
[]
|
||||
);
|
||||
const onDeleteLocation = useCallback((locationId: string) => {
|
||||
setLocations((prev) => prev.filter((l) => l.id !== locationId));
|
||||
}, []);
|
||||
|
||||
const onAddCoop = useCallback(
|
||||
(name: string, locationId: string) =>
|
||||
setCoops((prev) => [...prev, { id: `coop${Date.now()}`, name, locationId }]),
|
||||
[]
|
||||
);
|
||||
const onUpdateCoop = useCallback(
|
||||
(updatedCoop: Coop) =>
|
||||
setCoops((prev) => prev.map((c) => (c.id === updatedCoop.id ? updatedCoop : c))),
|
||||
[]
|
||||
);
|
||||
const onDeleteCoop = useCallback((coopId: string) => {
|
||||
setCoops((prev) => prev.filter((c) => c.id !== coopId));
|
||||
}, []);
|
||||
|
||||
const onAddFloor = useCallback(
|
||||
(name: string, coopId: string) =>
|
||||
setFloors((prev) => [...prev, { id: `floor${Date.now()}`, name, coopId }]),
|
||||
[]
|
||||
);
|
||||
const onUpdateFloor = useCallback(
|
||||
(updatedFloor: Floor) =>
|
||||
setFloors((prev) => prev.map((f) => (f.id === updatedFloor.id ? updatedFloor : f))),
|
||||
[]
|
||||
);
|
||||
const onDeleteFloor = useCallback((floorId: string) => {
|
||||
setFloors((prev) => prev.filter((f) => f.id !== floorId));
|
||||
}, []);
|
||||
|
||||
// Initialize from API data (used for LIVE mode)
|
||||
const initializeFromApi = useCallback(
|
||||
(data: { locations: Location[]; coops: Coop[]; floors: Floor[] }) => {
|
||||
setLocations(data.locations);
|
||||
setCoops(data.coops);
|
||||
setFloors(data.floors);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
locations,
|
||||
coops,
|
||||
floors,
|
||||
onAddLocation,
|
||||
onUpdateLocation,
|
||||
onDeleteLocation,
|
||||
onAddCoop,
|
||||
onUpdateCoop,
|
||||
onDeleteCoop,
|
||||
onAddFloor,
|
||||
onUpdateFloor,
|
||||
onDeleteFloor,
|
||||
initializeFromApi,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,482 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
Kpi,
|
||||
Alert,
|
||||
CoopZone,
|
||||
EnvironmentData,
|
||||
PopulationDataPoint,
|
||||
LiveCount,
|
||||
FeedSackLogEntry,
|
||||
ManualFeedSackEntry,
|
||||
BehaviorMetrics,
|
||||
BehaviorDataPoint,
|
||||
BehaviorAnomaly,
|
||||
WeightStatistics,
|
||||
WeightDataPoint,
|
||||
WeightDistributionPoint,
|
||||
WeightAlert,
|
||||
NetworkDevice,
|
||||
BandwidthHistoryPoint,
|
||||
BatchService,
|
||||
BatchJob,
|
||||
CycleData,
|
||||
MultiCoopChartPoint,
|
||||
MultiCoopWeightSummary,
|
||||
FeedWastageStats,
|
||||
WastageHotspot,
|
||||
FeedWastageAlert,
|
||||
WastageDataPoint,
|
||||
FeedScheduleRecommendation,
|
||||
FeedingEvent,
|
||||
ConsumptionPatternPoint,
|
||||
ScheduleInfluencingFactors,
|
||||
ErpSyncStats,
|
||||
ErpEndpoint,
|
||||
VetVisit,
|
||||
OcrDocument,
|
||||
ManualWeighingRecord,
|
||||
AiRecommendation,
|
||||
RecommendationStatus,
|
||||
CkaleWeightPoint,
|
||||
MultiCoopWeightData,
|
||||
FcrDataPoint,
|
||||
WeightDistributionStats,
|
||||
SensorWeightSummary,
|
||||
} from '../../types.ts';
|
||||
import { apiClient } from '../../services/apiService.ts';
|
||||
|
||||
import { cycleData as initialCycles, defaultPhases } from '../../mockData/cycleData.ts';
|
||||
|
||||
export const useStaticDataStore = () => {
|
||||
const [kpis, setKpis] = useState<Record<string, Kpi>>({});
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
const [coopZones, _setCoopZones] = useState<CoopZone[]>([]);
|
||||
const [environmentData, setEnvironmentData] = useState<EnvironmentData[]>([]);
|
||||
const [populationHistory, _setPopulationHistory] = useState<PopulationDataPoint[]>([]);
|
||||
const [liveCounts, setLiveCounts] = useState<LiveCount[]>([]);
|
||||
const [feedSackHistory, setFeedSackHistory] = useState<FeedSackLogEntry[]>([]);
|
||||
const [availableFeedSackCameras, setAvailableFeedSackCameras] = useState<string[]>([]);
|
||||
const [manualFeedSackEntries, setManualFeedSackEntries] = useState<ManualFeedSackEntry[]>([]);
|
||||
const [behaviorMetrics, _setBehaviorMetrics] = useState<BehaviorMetrics>({
|
||||
activityIndex: 0,
|
||||
stressLevel: { value: 0, status: 'ok' },
|
||||
eatingPattern: { value: 0, status: 'ok' },
|
||||
drinkingPattern: { value: 0, status: 'ok' },
|
||||
});
|
||||
const [behaviorHistory, _setBehaviorHistory] = useState<BehaviorDataPoint[]>([]);
|
||||
const [behaviorAnomalies, _setBehaviorAnomalies] = useState<BehaviorAnomaly[]>([]);
|
||||
const [initialWeight, setInitialWeight] = useState<WeightStatistics>({
|
||||
averageWeight: { value: 0, unit: 'g', status: 'ok' },
|
||||
avgDailyGain: { value: 0, unit: 'g/hari', status: 'ok' },
|
||||
uniformity: { value: 0, unit: '%', status: 'ok' },
|
||||
});
|
||||
const [weightHistory, setWeightHistory] = useState<WeightDataPoint[]>([]);
|
||||
const [weightDistribution, setWeightDistribution] = useState<WeightDistributionPoint[]>([]);
|
||||
const [weightAlerts, setWeightAlerts] = useState<WeightAlert[]>([]);
|
||||
const [networkDevices, _setNetworkDevices] = useState<NetworkDevice[]>([]);
|
||||
const [bandwidthHistory, _setBandwidthHistory] = useState<BandwidthHistoryPoint[]>([]);
|
||||
const [batchServices, _setBatchServices] = useState<BatchService[]>([]);
|
||||
const [batchJobs, _setBatchJobs] = useState<BatchJob[]>([]);
|
||||
const [cycles, setCycles] = useState<CycleData[]>(initialCycles);
|
||||
|
||||
const [feedWastageStats, _setFeedWastageStats] = useState<FeedWastageStats>({
|
||||
currentWastage: { value: 0, unit: '%', status: 'ok' },
|
||||
totalWastedKg: { value: 0, unit: 'kg' },
|
||||
estimatedLoss: { value: 0, unit: 'Rp' },
|
||||
criticalZones: { value: 0, unit: 'zona' },
|
||||
});
|
||||
const [wastageHotspots, _setWastageHotspots] = useState<WastageHotspot[]>([]);
|
||||
const [wastageAlerts, _setWastageAlerts] = useState<FeedWastageAlert[]>([]);
|
||||
const [wastageHistory, _setWastageHistory] = useState<WastageDataPoint[]>([]);
|
||||
|
||||
const [feedSchedule, _setFeedSchedule] = useState<FeedScheduleRecommendation>({
|
||||
nextFeedingTime: new Date(),
|
||||
lastFeedingTime: new Date(),
|
||||
reason: '',
|
||||
});
|
||||
const [feedingEvents, setFeedingEvents] = useState<FeedingEvent[]>([]);
|
||||
const [consumptionPattern, _setConsumptionPattern] = useState<ConsumptionPatternPoint[]>([]);
|
||||
const [scheduleFactors, _setScheduleFactors] = useState<ScheduleInfluencingFactors>({
|
||||
temperature: { value: 0, unit: '°C', status: 'ok' },
|
||||
humidity: { value: 0, unit: '%', status: 'ok' },
|
||||
activityLevel: { value: 0, unit: '%', status: 'ok' },
|
||||
});
|
||||
|
||||
const [erpSyncStats, _setErpSyncStats] = useState<ErpSyncStats>({
|
||||
status: 'Maintenance',
|
||||
recordsSyncedToday: 0,
|
||||
lastSync: new Date(),
|
||||
});
|
||||
const [erpEndpoints, _setErpEndpoints] = useState<ErpEndpoint[]>([]);
|
||||
|
||||
const [vetVisits, setVetVisits] = useState<VetVisit[]>([]);
|
||||
const [ocrDocuments, setOcrDocuments] = useState<OcrDocument[]>([]);
|
||||
const [manualWeighingData, setManualWeighingData] = useState<ManualWeighingRecord[]>([]);
|
||||
const [ckaleData, setCkaleData] = useState<CkaleWeightPoint[]>([]);
|
||||
|
||||
const [aiRecommendations, setAiRecommendations] = useState<AiRecommendation[]>([]);
|
||||
const [multiCoopWeightData, setMultiCoopWeightData] = useState<{
|
||||
target: { day: number; weight: number }[];
|
||||
coops: MultiCoopWeightData[];
|
||||
}>({ target: [], coops: [] });
|
||||
const [fcrComparisonData, setFcrComparisonData] = useState<FcrDataPoint[]>([]);
|
||||
|
||||
// Weight distribution date selection state - Initialize with yesterday's date
|
||||
const getYesterday = () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return yesterday;
|
||||
};
|
||||
|
||||
const [distributionSelectedDate, setDistributionSelectedDate] = useState<Date>(getYesterday());
|
||||
const [distributionStats, setDistributionStats] = useState<WeightDistributionStats | null>(null);
|
||||
const [sensorWeightSummary, setSensorWeightSummary] = useState<SensorWeightSummary[]>([]);
|
||||
|
||||
// New scale data states for revamped dashboard
|
||||
const [dailyWeightData, setDailyWeightData] = useState<any[]>([]); // DailyWeightPoint[]
|
||||
const [dailyWeightGainData, setDailyWeightGainData] = useState<any[]>([]); // DailyWeightGainPoint[]
|
||||
const [deviceWeightData, setDeviceWeightData] = useState<any[]>([]); // DeviceWeightData[]
|
||||
const [scaleDataLoading, setScaleDataLoading] = useState(true); // Start as true to prevent flash
|
||||
const [scaleDataLastUpdate, setScaleDataLastUpdate] = useState<Date>(new Date());
|
||||
const [dailyDataLoading, setDailyDataLoading] = useState(false); // Loading state for daily data section
|
||||
|
||||
// Load cycles from database on mount
|
||||
useEffect(() => {
|
||||
const loadCycles = async () => {
|
||||
try {
|
||||
const dbCycles = await apiClient.getCycles();
|
||||
// Transform database format to CycleData format
|
||||
const transformedCycles: CycleData[] = dbCycles.map((cycle: any) => ({
|
||||
id: cycle.id,
|
||||
totalDays: cycle.totalDays,
|
||||
currentDay: cycle.currentDay,
|
||||
startDate: new Date(cycle.startDate),
|
||||
endDate: cycle.endDate ? new Date(cycle.endDate) : undefined,
|
||||
chickInWeight: cycle.chickInWeight,
|
||||
docInCount: cycle.docInCount,
|
||||
phases: defaultPhases,
|
||||
status: cycle.status,
|
||||
}));
|
||||
setCycles(transformedCycles);
|
||||
} catch (error) {
|
||||
console.error('Failed to load cycles from database:', error);
|
||||
// Keep using initial mock data as fallback
|
||||
}
|
||||
};
|
||||
loadCycles();
|
||||
}, []);
|
||||
|
||||
const coopNamesForChart = useMemo(() => {
|
||||
if (!multiCoopWeightData || !multiCoopWeightData.coops) return [];
|
||||
return ['Target', ...multiCoopWeightData.coops.map((d) => d.coopName)];
|
||||
}, [multiCoopWeightData]);
|
||||
|
||||
const multiCoopChartData = useMemo<MultiCoopChartPoint[]>(() => {
|
||||
if (!multiCoopWeightData || !multiCoopWeightData.target || !multiCoopWeightData.coops)
|
||||
return [];
|
||||
const chartPoints: { [day: number]: MultiCoopChartPoint } = {};
|
||||
|
||||
const allData = [
|
||||
{ coopName: 'Target', history: multiCoopWeightData.target },
|
||||
...multiCoopWeightData.coops,
|
||||
];
|
||||
|
||||
allData.forEach((coop) => {
|
||||
coop.history.forEach((point) => {
|
||||
if (!chartPoints[point.day]) {
|
||||
chartPoints[point.day] = { day: point.day };
|
||||
}
|
||||
chartPoints[point.day][coop.coopName] = point.weight;
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(chartPoints).sort((a, b) => a.day - b.day);
|
||||
}, [multiCoopWeightData]);
|
||||
|
||||
const multiCoopSummaryData = useMemo<MultiCoopWeightSummary[]>(() => {
|
||||
if (!multiCoopWeightData || !multiCoopWeightData.coops || !multiCoopWeightData.target)
|
||||
return [];
|
||||
return multiCoopWeightData.coops.map((coop) => {
|
||||
const history = coop.history;
|
||||
if (history.length < 2) {
|
||||
const currentWeight = history[0]?.weight || 0;
|
||||
return {
|
||||
coopId: coop.coopId,
|
||||
coopName: coop.coopName,
|
||||
currentWeight,
|
||||
adg: 0,
|
||||
uniformity: 0,
|
||||
deviation: 0,
|
||||
};
|
||||
}
|
||||
const currentWeight = history[history.length - 1].weight;
|
||||
const previousWeight = history[history.length - 2].weight;
|
||||
const adg = currentWeight - previousWeight;
|
||||
|
||||
const targetWeightForDay =
|
||||
multiCoopWeightData.target.find((t) => t.day === history[history.length - 1].day)?.weight ||
|
||||
currentWeight;
|
||||
const deviation = currentWeight - targetWeightForDay;
|
||||
|
||||
// Mock uniformity
|
||||
const uniformity = 80 + deviation / 10 + (Math.random() - 0.5) * 5;
|
||||
|
||||
return {
|
||||
coopId: coop.coopId,
|
||||
coopName: coop.coopName,
|
||||
currentWeight,
|
||||
adg: Math.round(adg),
|
||||
uniformity: Math.round(uniformity),
|
||||
deviation: Math.round(deviation),
|
||||
};
|
||||
});
|
||||
}, [multiCoopWeightData]);
|
||||
|
||||
const onUpdateVetVisit = useCallback((updatedVisit: VetVisit) => {
|
||||
setVetVisits((prev) => prev.map((v) => (v.id === updatedVisit.id ? updatedVisit : v)));
|
||||
}, []);
|
||||
|
||||
const onAddVetVisit = useCallback((visitData: Omit<VetVisit, 'id' | 'status'>) => {
|
||||
const newVisit: VetVisit = {
|
||||
...visitData,
|
||||
id: `vet-${Date.now()}`,
|
||||
status: 'Scheduled',
|
||||
};
|
||||
setVetVisits((prev) => [...prev, newVisit]);
|
||||
}, []);
|
||||
|
||||
const onUpdateOcrDocument = useCallback((updatedDocument: OcrDocument) => {
|
||||
setOcrDocuments((prev) => prev.map((d) => (d.id === updatedDocument.id ? updatedDocument : d)));
|
||||
}, []);
|
||||
|
||||
const onAddFeedingEvent = useCallback((eventData: Omit<FeedingEvent, 'id' | 'status'>) => {
|
||||
const newEvent: FeedingEvent = {
|
||||
...eventData,
|
||||
id: `event-${Date.now()}`,
|
||||
status: eventData.time > new Date() ? 'upcoming' : 'completed',
|
||||
};
|
||||
setFeedingEvents((prev) => [...prev, newEvent]);
|
||||
}, []);
|
||||
|
||||
const onUpdateFeedingEvent = useCallback((updatedEvent: FeedingEvent) => {
|
||||
setFeedingEvents((prev) => prev.map((e) => (e.id === updatedEvent.id ? updatedEvent : e)));
|
||||
}, []);
|
||||
|
||||
const onUpdateManualWeighingRecord = useCallback((updatedRecord: ManualWeighingRecord) => {
|
||||
setManualWeighingData((prev) =>
|
||||
prev.map((r) => (r.date === updatedRecord.date ? updatedRecord : r))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onUpdateRecommendationStatus = useCallback((id: string, status: RecommendationStatus) => {
|
||||
setAiRecommendations((prev) => prev.map((rec) => (rec.id === id ? { ...rec, status } : rec)));
|
||||
}, []);
|
||||
|
||||
const onUpdateCycleDates = useCallback(
|
||||
async (
|
||||
cycleId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
chickInWeight?: number,
|
||||
docInCount?: number
|
||||
) => {
|
||||
// Update local state first for immediate UI feedback
|
||||
setCycles((prev) =>
|
||||
prev.map((cycle) => {
|
||||
if (cycle.id === cycleId) {
|
||||
const totalDays =
|
||||
Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||||
return { ...cycle, startDate, totalDays, endDate, chickInWeight, docInCount };
|
||||
}
|
||||
return cycle;
|
||||
})
|
||||
);
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
const totalDays =
|
||||
Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||||
const cycle = await apiClient.getCycleById(cycleId);
|
||||
await apiClient.updateCycle(cycleId, {
|
||||
totalDays,
|
||||
currentDay: cycle.currentDay,
|
||||
startDate: startDate.toISOString().split('T')[0],
|
||||
endDate: endDate.toISOString().split('T')[0],
|
||||
chickInWeight: chickInWeight || null,
|
||||
docInCount: docInCount || null,
|
||||
status: cycle.status,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update cycle in database:', error);
|
||||
// Could add error handling/rollback here
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onCreateCycle = useCallback(
|
||||
async (startDate: Date, docInCount?: number, chickInWeight?: number) => {
|
||||
const year = startDate.getFullYear();
|
||||
const month = String(startDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(startDate.getDate()).padStart(2, '0');
|
||||
const newId = `CYCLE-JBW-${year}-${month}-${day}`;
|
||||
|
||||
const totalDays = 42;
|
||||
const endDate = new Date(startDate);
|
||||
endDate.setDate(endDate.getDate() + totalDays - 1);
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const startMs = new Date(startDate).setHours(0, 0, 0, 0);
|
||||
const diffMs = today.getTime() - startMs;
|
||||
const currentDay =
|
||||
diffMs >= 0 ? Math.min(Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1, totalDays) : 0;
|
||||
|
||||
const newCycle: CycleData = {
|
||||
id: newId,
|
||||
totalDays,
|
||||
currentDay,
|
||||
startDate,
|
||||
endDate,
|
||||
chickInWeight,
|
||||
docInCount,
|
||||
phases: defaultPhases,
|
||||
status: 'Active',
|
||||
};
|
||||
|
||||
// Optimistic UI update: mark previous active as Completed, add new cycle
|
||||
setCycles((prev) => {
|
||||
const updated = prev.map((c) =>
|
||||
c.status === 'Active'
|
||||
? { ...c, status: 'Completed' as const, currentDay: c.totalDays }
|
||||
: c
|
||||
);
|
||||
return [...updated, newCycle];
|
||||
});
|
||||
|
||||
// Persist to database
|
||||
// NOTE: Backend automatically marks previous active cycles as Completed when creating a new Active cycle
|
||||
try {
|
||||
await apiClient.createCycle({
|
||||
id: newId,
|
||||
totalDays,
|
||||
currentDay,
|
||||
startDate: startDate.toISOString().split('T')[0],
|
||||
endDate: endDate.toISOString().split('T')[0],
|
||||
chickInWeight: chickInWeight || null,
|
||||
docInCount: docInCount || null,
|
||||
status: 'Active',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create cycle in database:', error);
|
||||
// Rollback optimistic update on error
|
||||
setCycles((prev) => prev.filter((c) => c.id !== newId));
|
||||
}
|
||||
},
|
||||
[cycles]
|
||||
);
|
||||
|
||||
// Fetch manual feed sack data by date range
|
||||
const fetchManualFeedSackData = useCallback(async (startDate: string, endDate: string) => {
|
||||
try {
|
||||
const entries = await apiClient.getManualFeedSackEntries(startDate, endDate);
|
||||
setManualFeedSackEntries(entries);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch manual feed sack entries:', error);
|
||||
setManualFeedSackEntries([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
alerts,
|
||||
coopZones,
|
||||
environmentData,
|
||||
populationHistory,
|
||||
liveCounts,
|
||||
feedSackHistory,
|
||||
availableFeedSackCameras,
|
||||
manualFeedSackEntries,
|
||||
behaviorMetrics,
|
||||
behaviorHistory,
|
||||
behaviorAnomalies,
|
||||
initialWeight,
|
||||
weightHistory,
|
||||
weightDistribution,
|
||||
weightAlerts,
|
||||
networkDevices,
|
||||
bandwidthHistory,
|
||||
batchServices,
|
||||
batchJobs,
|
||||
cycles,
|
||||
feedWastageStats,
|
||||
wastageHotspots,
|
||||
wastageAlerts,
|
||||
wastageHistory,
|
||||
feedSchedule,
|
||||
feedingEvents,
|
||||
consumptionPattern,
|
||||
scheduleFactors,
|
||||
multiCoopChartData,
|
||||
multiCoopSummaryData,
|
||||
coopNamesForChart,
|
||||
erpSyncStats,
|
||||
erpEndpoints,
|
||||
vetVisits,
|
||||
ocrDocuments,
|
||||
manualWeighingData,
|
||||
aiRecommendations,
|
||||
ckaleData,
|
||||
fcrComparisonData,
|
||||
// Weight distribution date selection state
|
||||
distributionSelectedDate,
|
||||
distributionStats,
|
||||
sensorWeightSummary,
|
||||
// New scale data for revamped dashboard
|
||||
dailyWeightData,
|
||||
dailyWeightGainData,
|
||||
deviceWeightData,
|
||||
scaleDataLoading,
|
||||
scaleDataLastUpdate,
|
||||
dailyDataLoading,
|
||||
setMultiCoopWeightData,
|
||||
// Expose setters needed for LIVE API integration on the weight dashboard
|
||||
setInitialWeight,
|
||||
setWeightHistory,
|
||||
setWeightDistribution,
|
||||
setWeightAlerts,
|
||||
setFcrComparisonData,
|
||||
setCkaleData,
|
||||
// Weight distribution date selection setters
|
||||
setDistributionSelectedDate,
|
||||
setDistributionStats,
|
||||
setSensorWeightSummary,
|
||||
// New scale data setters
|
||||
setDailyWeightData,
|
||||
setDailyWeightGainData,
|
||||
setDeviceWeightData,
|
||||
setScaleDataLoading,
|
||||
setScaleDataLastUpdate,
|
||||
setDailyDataLoading,
|
||||
// Expose setters for IoT, feed sack, and other API integrations
|
||||
setEnvironmentData,
|
||||
setFeedSackHistory,
|
||||
setAvailableFeedSackCameras,
|
||||
setManualFeedSackEntries,
|
||||
fetchManualFeedSackData,
|
||||
setAlerts,
|
||||
setKpis,
|
||||
setFeedingEvents,
|
||||
setVetVisits,
|
||||
setLiveCounts,
|
||||
onUpdateVetVisit,
|
||||
onUpdateOcrDocument,
|
||||
onAddFeedingEvent,
|
||||
onUpdateFeedingEvent,
|
||||
onAddVetVisit,
|
||||
onUpdateManualWeighingRecord,
|
||||
onUpdateRecommendationStatus,
|
||||
onUpdateCycleDates,
|
||||
onCreateCycle,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user