import React, { useState, useEffect, useMemo } from 'react'; import { useAppContext } from '../../context/AppContext.tsx'; import { apiClient } from '../../services/apiService.ts'; import { Kandang, MortalityRecord } from '../../types/counting.ts'; import ActivityLogPanel from '../common/ActivityLogPanel.tsx'; import SimplePopulationChart from './SimplePopulationChart.tsx'; import { PageAiInsight } from '../shared/PageAiInsight.tsx'; interface DataManagementPageProps { selectedKandangId: number | null; kandangs: Kandang[]; } interface MortalitySourceRecord { day: number; mortalityCount?: number; afkir?: number; panen?: number; beratPanen?: number; keterangan?: string; isEdited?: boolean; chickenCount?: number; population?: number; } const DataManagementPage: React.FC = ({ selectedKandangId, kandangs }) => { const { cycleData } = useAppContext(); const [records, setRecords] = useState([]); const [editingDay, setEditingDay] = useState(null); const [editValues, setEditValues] = useState({ mati: 0, afkir: 0, panen: 0, beratPanen: '' as string, chickenCount: undefined as number | undefined, keterangan: '', }); const [validationErrors, setValidationErrors] = useState>({}); const [searchDay, setSearchDay] = useState(''); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const [activityLogRefresh, setActivityLogRefresh] = useState(0); const [iotCurrentPopulation, setIotCurrentPopulation] = useState(null); // Note: kandangs and selectedKandangId now come from props (global header dropdown) // Fetch latest IOT Chicken Counting data (for selected kandang or all kandangs) useEffect(() => { const fetchLatestChickenCount = async () => { try { // Calculate date range for fetching latest data const today = new Date(); const startDate = new Date(cycleData.startDate); startDate.setHours(0, 0, 0, 0); today.setHours(23, 59, 59, 999); const startDateStr = startDate.toISOString().split('T')[0]; const todayStr = today.toISOString().split('T')[0]; // Fetch chicken counting data for selected kandang or all kandangs const chickenCounts = await apiClient.getChickenCounting( startDateStr, todayStr, selectedKandangId || undefined // Filter by kandang if one is selected ); if (chickenCounts && chickenCounts.length > 0) { // Get the most recent chicken count const sortedCounts = [...chickenCounts].sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); const latestCount = sortedCounts[0].totalCount; setIotCurrentPopulation(latestCount); } else { // No IOT data available setIotCurrentPopulation(null); } } catch (error) { console.error('Failed to fetch latest chicken counting data:', error); setIotCurrentPopulation(null); } }; fetchLatestChickenCount(); // Refresh IOT data every 2 minutes const interval = setInterval(fetchLatestChickenCount, 2 * 60 * 1000); return () => clearInterval(interval); }, [cycleData.startDate, selectedKandangId, kandangs]); // Initial population based on selected kandang or total const initialPopulation = useMemo(() => { if (selectedKandangId === null) { // "Semua Kandang" - sum all kandang doc_in_counts, fallback to cycle total const total = kandangs.reduce((sum, k) => sum + (k.docInCount || 0), 0); return total > 0 ? total : cycleData.docInCount || 20000; } const kandang = kandangs.find((k) => k.id === selectedKandangId); return kandang?.docInCount || 0; }, [selectedKandangId, kandangs, cycleData]); const isReadOnly = selectedKandangId === null; // Calculate records with deplesi flow const calculateRecords = React.useCallback( ( rawRecords: MortalitySourceRecord[], initialPop: number, chickenCountMap: Record, maxDay: number, actualCurrentDay: number ): MortalityRecord[] => { let previousStockAkhir = 0; let cumulativeMortality = 0; const result: MortalityRecord[] = []; for (let day = 0; day <= maxDay; day++) { const dbRecord = rawRecords.find((r) => r.day === day); const mati = dbRecord?.mortalityCount || 0; const afkir = dbRecord?.afkir || 0; const panen = dbRecord?.panen || 0; const beratPanen = dbRecord?.beratPanen || 0; const keterangan = dbRecord?.keterangan || ''; const isEdited = dbRecord?.isEdited || false; // Day 0: DOC In arrives (stockAwal = initialPop) // Day 1+: stockAwal = previous day's stockAkhir const stockAwal = day === 0 ? initialPop : previousStockAkhir; const stockAkhir = stockAwal - mati - afkir - panen; const chickenCount = chickenCountMap[day] !== undefined ? chickenCountMap[day] : dbRecord?.chickenCount; const persentase = chickenCount !== undefined && chickenCount !== null && stockAkhir > 0 ? (Math.min(chickenCount, stockAkhir) / Math.max(chickenCount, stockAkhir)) * 100 : 0; cumulativeMortality += mati + afkir; const date = new Date(cycleData.startDate); date.setDate(date.getDate() + day); const isToday = day === actualCurrentDay; const isFuture = day > actualCurrentDay; result.push({ day, date: date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric', }), stockAwal, mati, afkir, panen, beratPanen, stockAkhir, chickenCount, persentase: Math.round(persentase * 10) / 10, keterangan, mortality: mati, mortalityTotal: cumulativeMortality, chickenLife: stockAkhir, isEdited, isToday, isFuture, }); previousStockAkhir = stockAkhir; } return result; }, [cycleData.startDate] ); // Load mortality data from database useEffect(() => { const loadData = async () => { try { let rawRecords: MortalitySourceRecord[] = []; if (selectedKandangId !== null) { // Single kandang: get per-kandang records rawRecords = await apiClient.getMortalityRecords(cycleData.id, selectedKandangId); } else { // "Semua Kandang": use aggregated totals rawRecords = await apiClient.getMortalityTotals(cycleData.id); } // Calculate actual current day based on today's date const startDate = cycleData.startDate; const today = new Date(); today.setHours(0, 0, 0, 0); const startDateMs = new Date(startDate).setHours(0, 0, 0, 0); const todayMs = today.getTime(); const diffMs = todayMs - startDateMs; const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0; // Last inclusive 0-based day index matches Estimasi Panen (startDate + totalDays - 1) const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); const maxDay = Math.min(cycleData.currentDay ?? 0, lastCycleDay); // Fetch chicken counting data const chickenCountMap: Record = {}; try { const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + maxDay); const startDateStr = new Date(startDate).toISOString().split('T')[0]; const endDateStr = endDate.toISOString().split('T')[0]; const chickenCounts = await apiClient.getChickenCounting( startDateStr, endDateStr, selectedKandangId || undefined ); chickenCounts.forEach((cc: { date: string; totalCount: number }) => { // Parse date string in local timezone to avoid off-by-one errors const [year, month, day] = cc.date.split('T')[0].split('-').map(Number); const ccDate = new Date(year, month - 1, day); ccDate.setHours(0, 0, 0, 0); const dayNum = Math.floor((ccDate.getTime() - startDateMs) / (1000 * 60 * 60 * 24)); if (dayNum >= 0 && dayNum <= maxDay) { chickenCountMap[dayNum] = (chickenCountMap[dayNum] || 0) + cc.totalCount; } }); } catch (err) { console.warn('Failed to load chicken counting data:', err); } const calculatedRecords = calculateRecords( rawRecords, initialPopulation, chickenCountMap, maxDay, actualCurrentDay ); setRecords(calculatedRecords); } catch (error) { console.error('Failed to load mortality data:', error); setRecords([]); } }; loadData(); }, [ cycleData.id, cycleData.startDate, cycleData.currentDay, cycleData.totalDays, selectedKandangId, initialPopulation, calculateRecords, ]); const loadMortalityData = async () => { try { let rawRecords: Array<{ day: number; mortalityCount?: number; afkir?: number; panen?: number; beratPanen?: number; keterangan?: string; isEdited?: boolean; chickenCount?: number; }> = []; if (selectedKandangId !== null) { // Single kandang: get per-kandang records rawRecords = await apiClient.getMortalityRecords(cycleData.id, selectedKandangId); } else { // "Semua Kandang": use aggregated totals rawRecords = await apiClient.getMortalityTotals(cycleData.id); } // Calculate actual current day based on today's date const startDate = cycleData.startDate; const today = new Date(); today.setHours(0, 0, 0, 0); const startDateMs = new Date(startDate).setHours(0, 0, 0, 0); const todayMs = today.getTime(); const diffMs = todayMs - startDateMs; const actualCurrentDay = diffMs >= 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0; // Last inclusive 0-based day index matches Estimasi Panen (startDate + totalDays - 1) const lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1); const maxDay = Math.min(cycleData.currentDay ?? 0, lastCycleDay); // Fetch chicken counting data const chickenCountMap: Record = {}; try { const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + maxDay); const startDateStr = new Date(startDate).toISOString().split('T')[0]; const endDateStr = endDate.toISOString().split('T')[0]; const chickenCounts = await apiClient.getChickenCounting( startDateStr, endDateStr, selectedKandangId || undefined ); chickenCounts.forEach((cc: { date: string; totalCount: number }) => { // Parse date string in local timezone to avoid off-by-one errors const [year, month, day] = cc.date.split('T')[0].split('-').map(Number); const ccDate = new Date(year, month - 1, day); ccDate.setHours(0, 0, 0, 0); const dayNum = Math.floor((ccDate.getTime() - startDateMs) / (1000 * 60 * 60 * 24)); if (dayNum >= 0 && dayNum <= maxDay) { chickenCountMap[dayNum] = (chickenCountMap[dayNum] || 0) + cc.totalCount; } }); } catch (err) { console.warn('Failed to load chicken counting data:', err); } const calculatedRecords = calculateRecords( rawRecords, initialPopulation, chickenCountMap, maxDay, actualCurrentDay ); setRecords(calculatedRecords); } catch (error) { console.error('Failed to load mortality data:', error); setRecords([]); } }; // Edit handlers const handleEdit = (record: MortalityRecord) => { setEditingDay(record.day); setEditValues({ mati: record.mati, afkir: record.afkir, panen: record.panen, beratPanen: record.beratPanen !== undefined && record.beratPanen !== null ? record.beratPanen.toString() : '', chickenCount: record.chickenCount, keterangan: record.keterangan, }); setValidationErrors({}); }; const validateEditValues = (record: MortalityRecord): boolean => { const errors: Record = {}; const beratPanenValue = editValues.beratPanen === '' ? 0 : parseFloat(editValues.beratPanen); if (editValues.mati < 0) { errors.mati = 'Mati tidak boleh negatif'; } else if (editValues.mati > record.stockAwal) { errors.mati = `Mati tidak boleh lebih dari stock awal (${record.stockAwal})`; } if (editValues.afkir < 0) { errors.afkir = 'Afkir tidak boleh negatif'; } else if (editValues.afkir > record.stockAwal - editValues.mati) { errors.afkir = `Afkir tidak boleh lebih dari sisa (${record.stockAwal - editValues.mati})`; } if (editValues.panen < 0) { errors.panen = 'Panen tidak boleh negatif'; } else if (editValues.panen > record.stockAwal - editValues.mati - editValues.afkir) { errors.panen = `Panen tidak boleh lebih dari sisa (${record.stockAwal - editValues.mati - editValues.afkir})`; } if (editValues.chickenCount !== undefined && editValues.chickenCount < 0) { errors.chickenCount = 'Chicken counting tidak boleh negatif'; } if (Number.isNaN(beratPanenValue) || beratPanenValue < 0) { errors.beratPanen = 'Berat panen tidak boleh negatif'; } if (editValues.keterangan.length > 200) { errors.keterangan = 'Keterangan maksimal 200 karakter'; } setValidationErrors(errors); return Object.keys(errors).length === 0; }; const handleSave = async () => { if (editingDay === null) return; const record = records.find((r) => r.day === editingDay); if (!record) return; if (!validateEditValues(record)) { return; } try { await apiClient.updateMortalityRecord( cycleData.id, editingDay, editValues.mati, editValues.chickenCount, selectedKandangId || undefined, editValues.panen, editValues.keterangan, editValues.afkir, editValues.beratPanen === '' ? 0 : parseFloat(editValues.beratPanen) ); // Save chicken count to chicken_counting table if provided if (editValues.chickenCount !== undefined && selectedKandangId !== null) { const kandang = kandangs.find((k) => k.id === selectedKandangId); if (kandang) { const kandangSlug = kandang.name.toLowerCase().replace(/\s+/g, '-'); const date = new Date(cycleData.startDate); date.setDate(date.getDate() + editingDay); const dateStr = date.toISOString().split('T')[0]; await apiClient.submitChickenCounting( dateStr, kandangSlug, editValues.chickenCount, true ); } } setEditingDay(null); setValidationErrors({}); loadMortalityData(); // Trigger activity log refresh setActivityLogRefresh((prev) => prev + 1); } catch (error) { console.error('Failed to save:', error); alert('Gagal menyimpan data. Silakan coba lagi.'); } }; const handleCancel = () => { setEditingDay(null); setValidationErrors({}); }; // Filtered and sorted records const filteredRecords = useMemo(() => { let filtered = records; if (searchDay) { const searchNum = parseInt(searchDay, 10); if (!isNaN(searchNum)) { filtered = filtered.filter((r) => r.day === searchNum); } } const sorted = [...filtered].sort((a, b) => { if (sortOrder === 'asc') { return a.day - b.day; } else { return b.day - a.day; } }); return sorted; }, [records, searchDay, sortOrder]); // Statistics const stats = useMemo(() => { const latestRecord = records[records.length - 1]; const totalMortality = latestRecord?.mortalityTotal || 0; const totalPanen = records.reduce((sum, r) => sum + r.panen, 0); const totalBeratPanen = records.reduce((sum, r) => sum + (r.beratPanen || 0), 0); const mortalityRate = ((totalMortality / initialPopulation) * 100).toFixed(2); const lastDay = latestRecord?.day || 0; // Use IOT Chicken Counting data for all views (Semua Kandang or individual kandang) // Fall back to calculated population only when IOT data is unavailable let currentPopulation: number; if (iotCurrentPopulation !== null) { // Use latest IOT chicken count detection currentPopulation = iotCurrentPopulation; } else { // Calculate from mortality records when IOT data unavailable currentPopulation = latestRecord?.stockAkhir || initialPopulation; } return { totalMortality, currentPopulation, totalPanen, totalBeratPanen, mortalityRate, lastDay, }; }, [records, initialPopulation, iotCurrentPopulation]); // Badge for persentase const getPersentaseBadge = (persentase: number) => { if (!persentase) return -; const isGreen = persentase >= 95; const isYellow = persentase >= 90 && persentase < 95; const colorClass = isGreen ? 'bg-green-100 text-green-700' : isYellow ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'; return ( {persentase.toFixed(1)}% ); }; // Prepare chart data const chartData = useMemo(() => { return records.map((r) => { const date = new Date(cycleData.startDate); date.setDate(date.getDate() + r.day); const formattedDate = date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' }); return { date: formattedDate, population: r.stockAkhir, chickenCount: r.chickenCount || undefined, }; }); }, [records, cycleData.startDate]); return (
{/* Header */}

Hitung Ayam

Kelola dan edit data deplesi harian untuk siklus produksi aktif

{/* AI Insight */} {/* Kandang selection now handled by global header dropdown */} {/* Population Chart */}
k.id === selectedKandangId)?.name || ''}` } />
{/* Statistics Cards */}

Populasi Awal

{initialPopulation.toLocaleString()}

Populasi Saat Ini

{stats.currentPopulation.toLocaleString()}

Total Mortalitas

{stats.totalMortality.toLocaleString()}

Total Panen

{stats.totalPanen.toLocaleString()} ekor

{stats.totalBeratPanen.toFixed(2)} kg

Tingkat Mortalitas

{stats.mortalityRate}%

{/* Filters and Sorting */}
setSearchDay(e.target.value)} placeholder="Masukkan nomor hari..." className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{/* Read-only banner for "Semua Kandang" */} {isReadOnly && kandangs.length > 0 && (

Mode Lihat Saja (Total Semua Kandang)

Untuk mengedit data, pilih kandang tertentu di tab di atas.

)} {/* Data Table */}
{filteredRecords.length === 0 ? (
Tidak ada data yang ditemukan
) : ( filteredRecords.map((record) => { const isEditing = !isReadOnly && editingDay === record.day; const cardTone = record.isToday ? 'ring-2 ring-blue-200 bg-blue-50/40' : record.panen > 0 ? 'ring-1 ring-purple-200 bg-white' : 'bg-white'; return (
Hari {record.day} {record.isToday && ( Hari ini )} {record.isFuture && ( Mendatang )} {record.isEdited && ( Diubah )}
{record.date}
{!isReadOnly && (
{isEditing ? (
) : ( )}
)}
Stock awal
{record.stockAwal.toLocaleString()}
Mati
{isEditing ? (
setEditValues({ ...editValues, mati: parseInt(e.target.value) || 0, }) } className="mt-1 w-full rounded-lg border border-red-200 bg-white px-2 py-2 text-right text-[11px] focus:border-red-400 focus:outline-none focus:ring-2 focus:ring-red-200 md:px-3 md:text-sm" /> {validationErrors.mati && (
{validationErrors.mati}
)}
) : (
{record.mati.toLocaleString()}
)}
AFKIR
{isEditing ? (
setEditValues({ ...editValues, afkir: parseInt(e.target.value) || 0, }) } className="mt-1 w-full rounded-lg border border-orange-200 bg-white px-2 py-2 text-right text-[11px] focus:border-orange-400 focus:outline-none focus:ring-2 focus:ring-orange-200 md:px-3 md:text-sm" /> {validationErrors.afkir && (
{validationErrors.afkir}
)}
) : (
{record.afkir.toLocaleString()}
)}
Panen
{isEditing ? (
setEditValues({ ...editValues, panen: parseInt(e.target.value) || 0, }) } className="mt-1 w-full rounded-lg border border-purple-200 bg-white px-2 py-2 text-right text-[11px] focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 md:px-3 md:text-sm" /> {validationErrors.panen && (
{validationErrors.panen}
)}
) : (
{record.panen.toLocaleString()}
)}
Berat panen
{isEditing ? (
setEditValues({ ...editValues, beratPanen: e.target.value, }) } className="mt-1 w-full rounded-lg border border-purple-200 bg-white px-2 py-2 text-right text-[11px] focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 md:px-3 md:text-sm" /> {validationErrors.beratPanen && (
{validationErrors.beratPanen}
)}
) : (
{record.beratPanen ? record.beratPanen.toFixed(2) : '0.00'} kg
)}
Stock akhir
{isEditing ? ( record.stockAwal - editValues.mati - editValues.afkir - editValues.panen ).toLocaleString() : record.stockAkhir.toLocaleString()}
Chicken Counting
{isEditing ? (
setEditValues({ ...editValues, chickenCount: e.target.value ? parseInt(e.target.value) : undefined, }) } placeholder="0" className="mt-1 w-full rounded-lg border border-blue-200 bg-white px-2 py-2 text-right text-[11px] focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-200 md:px-3 md:text-sm" /> {validationErrors.chickenCount && (
{validationErrors.chickenCount}
)}
) : (
{record.chickenCount?.toLocaleString() || '-'}
)}
Persentase
{getPersentaseBadge(record.persentase)}
Keterangan
{isEditing ? (