first commit

This commit is contained in:
Alberto-Audrix committed 2026-09-08 15:21:23 +07:00
commit b54624be96
226 files changed
+108840

No files matched your search

+850
View File
@@ -0,0 +1,850 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { api, errorMessage } from '../../services/apiClient.ts';
import type { AIInsight, ChickenCounting, KPI, ManualInput } from '../../types/api.ts';
import { useFarm } from '../../context/FarmContext.tsx';
import { Banner, EmptyFarm, PageHeader, Spinner } from '../ui/Feedback.tsx';
import {
addDaysIso,
cycleDayForDate,
formatDateId,
formatDateRange,
formatNumber,
resolveCycleStatus,
todayIso,
} from '../../utils/format.ts';
import PopulationTrendChart from './PopulationTrendChart.tsx';
import CountingAccuracyWarningBanner from './CountingAccuracyWarningBanner.tsx';
import { getCountingAccuracyWarning } from '../../utils/countingAccuracyWarning.ts';
type Row = {
day: number;
date: string;
dateLabel: string;
stockAwal: number;
mati: number;
matiIot: number;
akurasiMati: number;
panen: number;
beratPanen: number;
stockAkhir: number;
chickenCount?: number;
persentase: number;
mortalityTotal: number;
harvestTotal: number;
harvestWeightTotal: number;
isEdited: boolean;
isToday: boolean;
isFuture: boolean;
};
function mortalityAccuracy(manual: number, iot: number): number {
if (manual === 0 && iot === 0) return 100;
const denom = Math.max(manual, iot);
if (denom <= 0) return 100;
return Math.round((Math.min(manual, iot) / denom) * 1000) / 10;
}
const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
const { selectedCycle, selectedKandang, selectedSite } = useFarm();
const [kpis, setKpis] = useState<KPI[]>([]);
const [countings, setCountings] = useState<ChickenCounting[]>([]);
const [manualRows, setManualRows] = useState<ManualInput[]>([]);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [syncing, setSyncing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [searchDay, setSearchDay] = useState<string>('');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const isClosedCycle = !selectedCycle || resolveCycleStatus(selectedCycle) !== 'active';
const loadData = useCallback(async (cycleId: number) => {
setLoading(true);
setError(null);
try {
const [k, c, m, i] = await Promise.all([
api.kpis.list({ cycle_id: cycleId }),
api.countings.list({ cycle_id: cycleId }),
api.manualInputs.list({ cycle_id: cycleId }),
api.insights.list({ cycle_id: cycleId }),
]);
setKpis(k);
setCountings(c);
setManualRows(m);
setInsights(i);
} catch (err) {
setError(errorMessage(err));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!selectedCycle) {
setKpis([]);
setCountings([]);
setManualRows([]);
setInsights([]);
return;
}
void loadData(selectedCycle.id);
}, [selectedCycle, loadData]);
const handleSync = useCallback(async () => {
if (!selectedCycle) return;
setSyncing(true);
setError(null);
setNotice(null);
try {
await api.countings.sync({ cycle_id: selectedCycle.id });
setNotice('Sinkron chicken counting edge berhasil.');
await loadData(selectedCycle.id);
} catch (err) {
setError(errorMessage(err));
} finally {
setSyncing(false);
}
}, [selectedCycle, loadData]);
// Build daily rows through calendar today (not publish-gated current_day).
const rows = useMemo<Row[]>(() => {
if (!selectedCycle) return [];
const countingByDate = new Map(countings.map((c) => [c.date, c]));
const manualByDate = new Map(manualRows.map((m) => [m.date, m]));
const kpiByDate = new Map(kpis.map((k) => [k.date, k]));
const lastCycleDay = Math.max(0, (selectedCycle.total_days || 1) - 1);
const endDay = cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date);
const calendarDay = cycleDayForDate(selectedCycle.start_date, todayIso());
const maxDay = Math.min(calendarDay, lastCycleDay, endDay);
const today = todayIso();
let previousStockAkhir = 0;
let cumulativeMortality = 0;
let cumulativeHarvest = 0;
let cumulativeHarvestWeight = 0;
const result: Row[] = [];
for (let day = 0; day <= maxDay; day++) {
const date = addDaysIso(selectedCycle.start_date, day);
const kpi = kpiByDate.get(date);
const manual = manualByDate.get(date);
const counting = countingByDate.get(date);
const stockAwal = day === 0 ? selectedCycle.doc_in_count || 0 : previousStockAkhir;
const mati = manual?.mortality_manual ?? kpi?.mortality ?? 0;
const matiIot = counting?.mortality_count ?? 0;
const akurasiMati = mortalityAccuracy(mati, matiIot);
const panen = manual?.harvest_manual ?? kpi?.harvest ?? 0;
// Database menyimpan berat panen dalam gram; tampilkan selalu dalam kg.
const beratPanen = (manual?.harvest_weight_manual ?? kpi?.harvest_weight ?? 0) / 1000;
const stockAkhir = kpi?.chicken_life ?? Math.max(0, stockAwal - mati - panen);
const chickenCount = counting?.total_count;
// Akurasi Chicken Counting (IOT) terhadap stock akhir manual:
// (nilai kecil / nilai besar) × 100%. Tanpa data IOT, tetap 0 (tampil "-").
const persentase =
chickenCount !== undefined && chickenCount !== null && stockAkhir > 0
? (Math.min(chickenCount, stockAkhir) / Math.max(chickenCount, stockAkhir)) * 100
: 0;
cumulativeMortality += mati;
cumulativeHarvest += panen;
cumulativeHarvestWeight += beratPanen;
result.push({
day,
date,
dateLabel: formatDateId(date),
stockAwal,
mati,
matiIot,
akurasiMati,
panen,
beratPanen,
stockAkhir,
chickenCount,
persentase: Math.round(persentase * 10) / 10,
mortalityTotal: kpi?.mortality_total ?? cumulativeMortality,
harvestTotal: kpi?.harvest_total ?? cumulativeHarvest,
harvestWeightTotal:
kpi?.harvest_weight_total !== undefined
? kpi.harvest_weight_total / 1000
: cumulativeHarvestWeight,
isEdited: !!manual,
isToday: date === today,
isFuture: date > today,
});
previousStockAkhir = stockAkhir;
}
return result;
}, [kpis, countings, manualRows, selectedCycle]);
const stats = useMemo(() => {
const initialPopulation = selectedCycle?.doc_in_count || 0;
const latest = rows[rows.length - 1];
const totalMortality = latest?.mortalityTotal ?? 0;
const totalPanen = latest?.harvestTotal ?? 0;
const totalBeratPanen = latest?.harvestWeightTotal ?? 0;
const currentPopulation = latest?.stockAkhir ?? initialPopulation;
const mortalityRate = initialPopulation
? ((totalMortality / initialPopulation) * 100).toFixed(2)
: '0.00';
return {
initialPopulation,
totalMortality,
currentPopulation,
totalPanen,
totalBeratPanen,
mortalityRate,
};
}, [rows, selectedCycle]);
const chartData = useMemo(
() =>
rows.map((r) => ({
date: r.dateLabel,
population: r.stockAkhir,
chickenCount: r.chickenCount ?? undefined,
})),
[rows]
);
const countingAccuracyWarning = useMemo(() => {
if (!selectedCycle) return null;
return getCountingAccuracyWarning({
cycle: selectedCycle,
countings,
kpis,
manualInputs: manualRows,
});
}, [selectedCycle, countings, kpis, manualRows]);
const filteredRecords = useMemo(() => {
let filtered = rows;
if (searchDay) {
const searchNum = parseInt(searchDay, 10);
if (!Number.isNaN(searchNum)) {
filtered = filtered.filter((r) => r.day === searchNum);
}
}
return [...filtered].sort((a, b) => (sortOrder === 'asc' ? a.day - b.day : b.day - a.day));
}, [rows, searchDay, sortOrder]);
const getPersentaseBadge = (persentase: number) => {
if (!persentase) return <span className="text-gray-400">-</span>;
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 (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{persentase.toFixed(1)}%
</span>
);
};
const getAkurasiMatiBadge = (akurasi: number) => {
const isGreen = akurasi >= 95;
const isYellow = akurasi >= 90 && akurasi < 95;
const colorClass = isGreen
? 'bg-green-100 text-green-700'
: isYellow
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700';
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{akurasi.toFixed(1)}%
</span>
);
};
if (!selectedCycle) return <EmptyFarm onSettings={onSettings} />;
return (
<div>
{/* Header */}
<PageHeader
title="Hitung Ayam"
icon={<i className="fa-solid fa-calculator" />}
subtitle={`Lihat data deplesi harian untuk siklus produksi · ${selectedSite?.site_name ?? ''} · ${selectedKandang?.kandang_name ?? ''} · Siklus ${formatDateRange(selectedCycle.start_date, selectedCycle.end_date)}`}
actions={
<button
type="button"
onClick={handleSync}
disabled={syncing || isClosedCycle}
className="bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-semibold disabled:opacity-60 hover:bg-red-700 transition-colors"
>
{syncing ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
Sync...
</>
) : (
<>
<i className="fa-solid fa-arrows-rotate mr-2"></i>
Sync
</>
)}
</button>
}
/>
{countingAccuracyWarning && (
<CountingAccuracyWarningBanner warning={countingAccuracyWarning} />
)}
{/* AI Insight */}
<section className="mb-6">
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-wand-magic-sparkles text-purple-500 mr-2"></i>
AI Insight
</h2>
{insights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{insights.slice(0, 4).map((item) => (
<article
key={item.id}
className="bg-white rounded-xl shadow-sm p-5 border border-gray-100"
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-wide text-gray-400">{item.section}</p>
<h3 className="font-semibold text-gray-800 mt-1">{item.alert || 'Insight'}</h3>
</div>
<span className="text-xs text-gray-400">{item.session}</span>
</div>
<p className="text-sm text-gray-600 mt-3 whitespace-pre-wrap">
{item.insight_text}
</p>
</article>
))}
</div>
)}
</section>
{/* Population Chart */}
<div className="mb-6">
<PopulationTrendChart
data={chartData}
title={`Tren Populasi - ${selectedKandang?.kandang_name || ''}`}
/>
</div>
{error && (
<div className="mb-4">
<Banner kind="error">{error}</Banner>
</div>
)}
{notice && (
<div className="mb-4">
<Banner kind="success">{notice}</Banner>
</div>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-blue-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Populasi Awal</p>
<p className="text-xl font-bold text-gray-800">
{formatNumber(stats.initialPopulation, 0)}
</p>
</div>
<i className="fa-solid fa-egg text-blue-500 text-2xl"></i>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-green-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Populasi Saat Ini</p>
<p className="text-xl font-bold text-green-600">
{formatNumber(stats.currentPopulation, 0)}
</p>
</div>
<i className="fa-solid fa-drumstick-bite text-green-500 text-2xl"></i>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-red-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Total Mortalitas</p>
<p className="text-xl font-bold text-red-600">
{formatNumber(stats.totalMortality, 0)}
</p>
</div>
<i className="fa-solid fa-heart-crack text-red-500 text-2xl"></i>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-purple-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Total Panen</p>
<p className="text-xl font-bold text-purple-700">
{formatNumber(stats.totalPanen, 0)} ekor
</p>
<p className="text-sm text-purple-600 mt-1">
{formatNumber(stats.totalBeratPanen, 2)} kg
</p>
</div>
<i className="fa-solid fa-box text-purple-500 text-2xl"></i>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-orange-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Tingkat Mortalitas</p>
<p className="text-xl font-bold text-orange-600">{stats.mortalityRate}%</p>
</div>
<i className="fa-solid fa-chart-line text-orange-500 text-2xl"></i>
</div>
</div>
</div>
{loading ? (
<Spinner />
) : (
<>
{/* Filters and Sorting */}
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-2">
<i className="fa-solid fa-search mr-2"></i>Cari Hari
</label>
<input
type="number"
value={searchDay}
onChange={(e) => 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"
/>
</div>
<div className="flex items-end">
<div className="flex gap-2">
<button
onClick={() => setSortOrder('asc')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
sortOrder === 'asc'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<i className="fa-solid fa-arrow-up mr-2"></i>
Terlama
</button>
<button
onClick={() => setSortOrder('desc')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
sortOrder === 'desc'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<i className="fa-solid fa-arrow-down mr-2"></i>
Terbaru
</button>
</div>
</div>
</div>
</div>
{isClosedCycle && (
<div className="mb-6 bg-amber-50 border-l-4 border-amber-400 rounded-lg p-4">
<div className="flex items-center">
<i className="fa-solid fa-eye text-amber-500 text-xl mr-4"></i>
<div>
<p className="text-sm text-amber-800 font-semibold">
Mode Lihat Saja (Siklus Selesai)
</p>
</div>
</div>
</div>
)}
{/* Data Table */}
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<div className="space-y-4 p-4 md:hidden">
{filteredRecords.length === 0 ? (
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-6 py-12 text-center text-gray-500">
<i className="fa-solid fa-inbox text-4xl mb-3 block text-gray-300"></i>
Tidak ada data yang ditemukan
</div>
) : (
filteredRecords.map((record) => {
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 (
<article
key={record.day}
className={`rounded-2xl border border-gray-200 shadow-sm overflow-hidden ${cardTone}`}
>
<div className="flex items-start justify-between gap-3 px-4 py-4 bg-gray-50/80">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center gap-1 rounded-full bg-blue-600 px-3 py-1 text-xs font-semibold text-white">
Hari {record.day}
</span>
{record.isToday && (
<span className="rounded-full bg-blue-100 px-2.5 py-1 text-[11px] font-semibold text-blue-700">
Hari ini
</span>
)}
{record.isFuture && (
<span className="rounded-full bg-gray-200 px-2.5 py-1 text-[11px] font-semibold text-gray-700">
Mendatang
</span>
)}
{record.isEdited && (
<span className="rounded-full bg-amber-100 px-2.5 py-1 text-[11px] font-semibold text-amber-700">
Diubah
</span>
)}
</div>
<div className="mt-2 text-sm font-medium text-gray-700">
{record.dateLabel}
</div>
</div>
</div>
<div className="p-4">
<div className="grid grid-cols-2 gap-3">
<div className="rounded-xl bg-slate-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
Stock awal
</div>
<div className="text-sm font-semibold text-slate-800">
{formatNumber(record.stockAwal, 0)}
</div>
</div>
<div className="rounded-xl bg-red-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-red-500">
Mati
</div>
<div className="text-[11px] font-semibold text-red-600 md:text-sm">
{formatNumber(record.mati, 0)}
</div>
</div>
<div className="rounded-xl bg-rose-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-rose-500">
Mati IoT
</div>
<div className="text-[11px] font-semibold text-rose-600 md:text-sm">
{formatNumber(record.matiIot, 0)}
</div>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-emerald-600">
Akurasi Mati
</div>
<div className="mt-1">{getAkurasiMatiBadge(record.akurasiMati)}</div>
</div>
<div className="rounded-xl bg-purple-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-purple-500">
Panen
</div>
<div className="text-[11px] font-semibold text-purple-600 md:text-sm">
{formatNumber(record.panen, 0)}
</div>
</div>
<div className="rounded-xl bg-purple-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-purple-500">
Berat panen (kg)
</div>
<div className="text-[11px] font-semibold text-purple-600 md:text-sm">
{record.beratPanen ? formatNumber(record.beratPanen, 2) : '0.00'} kg
</div>
</div>
<div className="rounded-xl bg-slate-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
Stock akhir
</div>
<div className="text-[11px] font-semibold text-slate-900 md:text-sm">
{formatNumber(record.stockAkhir, 0)}
</div>
</div>
<div className="rounded-xl bg-blue-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-blue-500">
Chicken Counting
</div>
<div className="text-[11px] font-semibold text-blue-600 md:text-sm">
{record.chickenCount !== undefined
? formatNumber(record.chickenCount, 0)
: '-'}
</div>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
<div className="text-[11px] font-semibold uppercase tracking-wide text-emerald-600">
Persentase
</div>
<div className="mt-1">{getPersentaseBadge(record.persentase)}</div>
</div>
</div>
</div>
</article>
);
})
)}
</div>
<div className="hidden md:block">
<div className="overflow-x-auto">
<table className="min-w-[1280px] lg:min-w-[1380px] w-full border-collapse table-auto">
<thead className="bg-gray-50">
{/* Top header row with grouped columns */}
<tr className="border-b border-gray-300">
<th
rowSpan={2}
className="w-16 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 border-r border-gray-300"
>
Hari
</th>
<th
rowSpan={2}
className="w-24 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 border-r border-gray-300"
>
Tanggal
</th>
<th
colSpan={4}
className="px-2 py-2 text-center text-xs font-bold uppercase text-white bg-blue-400 border-r border-gray-300"
>
Deplesi Daily
</th>
<th
colSpan={2}
className="px-2 py-2 text-center text-xs font-bold uppercase text-white bg-purple-400 border-r border-gray-300"
>
PANEN
</th>
<th
rowSpan={2}
className="w-24 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 border-r border-gray-300"
>
Stock akhir
</th>
<th
rowSpan={2}
className="w-28 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 border-r border-gray-300"
>
Chicken Counting (IOT)
</th>
<th
rowSpan={2}
className="w-20 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700"
>
Persentase
</th>
</tr>
{/* Second header row with individual column names */}
<tr className="border-b border-gray-200">
<th className="w-24 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-blue-50 border-r border-gray-300">
Stock awal
</th>
<th className="w-16 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-blue-50 border-r border-gray-300">
Mati
</th>
<th className="w-16 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-blue-50 border-r border-gray-300">
Mati IoT
</th>
<th className="w-20 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-blue-50 border-r border-gray-300">
Akurasi Mati
</th>
<th className="w-20 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-purple-50 border-r border-gray-300">
JUMLAH PANEN
</th>
<th className="w-24 px-2 py-2 text-center text-[11px] font-semibold uppercase text-gray-700 bg-purple-50 border-r border-gray-300">
BERAT PANEN (KG)
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredRecords.length === 0 ? (
<tr>
<td
colSpan={11}
className="px-6 py-12 text-center text-gray-500"
>
<i className="fa-solid fa-inbox text-4xl mb-3 block text-gray-300"></i>
Tidak ada data yang ditemukan
</td>
</tr>
) : (
filteredRecords.map((record, index) => {
const rowClass = `hover:bg-gray-100 transition-colors ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-50'
} ${record.panen > 0 ? 'border-l-4 border-purple-400' : ''}`;
return (
<tr key={record.day} className={rowClass}>
{/* Hari */}
<td className="px-2 py-2 whitespace-nowrap border-r border-gray-200 text-center">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-800 md:h-9 md:w-9 md:text-sm">
{record.day}
</span>
</td>
{/* Tanggal */}
<td className="px-2 py-2 whitespace-nowrap border-r border-gray-200 text-center text-[11px] text-gray-700 md:text-sm">
{record.dateLabel}
</td>
{/* Stock awal */}
<td className="px-2 py-2 whitespace-nowrap border-r border-gray-200 text-center text-[11px] text-gray-500 md:text-sm">
{formatNumber(record.stockAwal, 0)}
</td>
{/* Mati */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
<span className="text-[11px] font-semibold text-red-600 md:text-sm">
{formatNumber(record.mati, 0)}
</span>
</td>
{/* Mati IoT */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
<span className="text-[11px] font-semibold text-rose-600 md:text-sm">
{formatNumber(record.matiIot, 0)}
</span>
</td>
{/* Akurasi Mati */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
{getAkurasiMatiBadge(record.akurasiMati)}
</td>
{/* JUMLAH PANEN */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
<span className="text-[11px] font-semibold text-purple-600 md:text-sm">
{formatNumber(record.panen, 0)}
</span>
</td>
{/* BERAT PANEN */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
<span className="text-[11px] font-semibold text-purple-600 md:text-sm">
{record.beratPanen ? formatNumber(record.beratPanen, 2) : '0.00'}
</span>
</td>
{/* Stock akhir */}
<td className="px-2 py-2 whitespace-nowrap text-center text-[11px] font-bold text-gray-800 border-r border-gray-200 md:text-sm">
{formatNumber(record.stockAkhir, 0)}
</td>
{/* Chicken Counting */}
<td className="px-2 py-2 whitespace-nowrap text-center border-r border-gray-200">
<span className="text-[11px] font-semibold text-blue-600 md:text-sm">
{record.chickenCount !== undefined
? formatNumber(record.chickenCount, 0)
: '-'}
</span>
</td>
{/* Persentase */}
<td className="px-2 py-2 whitespace-nowrap text-center">
{getPersentaseBadge(record.persentase)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</div>
{/* Info Section */}
<div className="mt-6 bg-blue-50 border-l-4 border-blue-500 rounded-lg p-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<i className="fa-solid fa-info-circle text-blue-500 text-2xl"></i>
</div>
<div className="ml-4">
<h3 className="text-sm font-semibold text-blue-800 mb-2">Informasi</h3>
<ul className="text-sm text-blue-700 space-y-1">
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>Data hanya ditampilkan hingga
hari ini (dihitung dari tanggal mulai siklus)
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Hari 0:</strong> Stock awal = Jumlah DOC In (ayam masuk)
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Hari 1+:</strong> Stock awal = Stock akhir hari sebelumnya
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Stock akhir</strong> = Stock awal - Mati - panen (otomatis terhitung)
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Mati:</strong> Mortalitas harian manual | <strong>Mati IoT:</strong>{' '}
mortalitas dari edge API (kosong = 0)
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Akurasi Mati</strong> = (nilai kecil / nilai besar) × 100% antara Mati
manual dan Mati IoT (keduanya 0 → 100%)
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>panen:</strong> Ayam dipanen
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Persentase</strong> = (nilai kecil / nilai besar) × 100% untuk konsep
akurasi hitungan IOT terhadap stock akhir manual
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>
<strong>Chicken Counting (IOT)</strong> = hasil hitung otomatis dari IOT
</li>
<li>
<i className="fa-solid fa-circle text-xs mr-2"></i>Baris dengan border ungu
menandakan ada panen pada hari tersebut
</li>
</ul>
</div>
</div>
</div>
</>
)}
</div>
);
};
export default DataManagementPage;