first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
commit 32a36cceff
444 files changed
+67186

No files matched your search

File diff suppressed because it is too large. Load diff
+40
View File
@@ -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,
};
};
+118
View File
@@ -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,
};
};
+27
View File
@@ -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,
};
};
+63
View File
@@ -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,
};
};
+76
View File
@@ -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,
};
};
+482
View File
@@ -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,
};
};