117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { errorMessage } from '../../services/apiClient.ts';
|
|
import type { View } from '../../types/common.ts';
|
|
import { useFarm } from '../../context/FarmContext.tsx';
|
|
import { Banner, Spinner } from '../ui/Feedback.tsx';
|
|
import { formatRatio } from '../../utils/format.ts';
|
|
import {
|
|
loadKandangRanking,
|
|
type KandangRankingRow,
|
|
} from '../../utils/kandangRanking.ts';
|
|
|
|
type Props = {
|
|
setView: (view: View) => void;
|
|
};
|
|
|
|
const KandangRankingTop5: React.FC<Props> = ({ setView }) => {
|
|
const {
|
|
selectedSite,
|
|
setSelectedSiteId,
|
|
setSelectedKandangId,
|
|
setSelectedCycleId,
|
|
} = useFarm();
|
|
const [rows, setRows] = useState<KandangRankingRow[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const openEef = (row: KandangRankingRow) => {
|
|
setSelectedSiteId(row.siteId);
|
|
setSelectedKandangId(row.kandangId);
|
|
setSelectedCycleId(row.cycleId);
|
|
setView('eefDashboard');
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!selectedSite) {
|
|
setRows([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const next = await loadKandangRanking({ sites: [selectedSite], months: 3 });
|
|
if (!cancelled) setRows(next.slice(0, 5));
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setError(errorMessage(err));
|
|
setRows([]);
|
|
}
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [selectedSite]);
|
|
|
|
if (!selectedSite) return null;
|
|
|
|
return (
|
|
<section className="mt-8 bg-white rounded-xl shadow-sm border border-gray-100 p-5">
|
|
<div className="flex flex-wrap items-start justify-between gap-3 mb-4">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-gray-800">Peringkat EEF</h2>
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
Top 5 · {selectedSite.site_name} · 3 bulan terakhir (siklus tertutup)
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="text-sm font-semibold text-red-600 hover:text-red-700"
|
|
onClick={() => setView('kandangRanking')}
|
|
>
|
|
Lihat semua
|
|
</button>
|
|
</div>
|
|
|
|
{error && <Banner kind="error">{error}</Banner>}
|
|
{loading ? (
|
|
<Spinner />
|
|
) : rows.length === 0 ? (
|
|
<p className="text-sm text-gray-500">Tidak ada siklus tertutup dalam 3 bulan terakhir.</p>
|
|
) : (
|
|
<ul className="divide-y divide-gray-100">
|
|
{rows.map((row) => (
|
|
<li
|
|
key={row.cycleId}
|
|
className="py-3 flex items-center justify-between gap-3 cursor-pointer hover:bg-gray-50 -mx-2 px-2 rounded-lg"
|
|
onClick={() => openEef(row)}
|
|
>
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<span className="w-7 h-7 rounded-full bg-red-50 text-red-700 text-xs font-bold flex items-center justify-center flex-shrink-0">
|
|
{row.rank > 0 ? row.rank : '—'}
|
|
</span>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-semibold text-gray-800 truncate">{row.kandangName}</p>
|
|
<p className="text-xs text-gray-500">
|
|
FCR {row.fcr == null ? 'N/A' : formatRatio(row.fcr, 3)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm font-bold text-gray-900 flex-shrink-0">
|
|
{row.eef == null ? 'N/A' : formatRatio(row.eef, 3)}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default KandangRankingTop5;
|