first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,116 @@
|
||||
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;
|
||||
Reference in new issue
Block a user