Files
dashboard/components/ManualWeighingPage.tsx
T

228 lines
10 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { useAppContext } from '../context/AppContext.tsx';
import { FileInvoiceIcon, BrainIcon } from './icons/Icons.tsx';
import WeighingRecordCard from './manualWeighing/WeighingRecordCard.tsx';
import { getTargetWeightForDay } from '../utils/weightUtils.ts';
import { ManualWeighingRecord } from '../types.ts';
import AiWeighingAnalysisModal from './manualWeighing/AiWeighingAnalysisModal.tsx';
import ManualWeighingStats from './manualWeighing/ManualWeighingStats.tsx';
import ManualWeighingAnalysis from './manualWeighing/ManualWeighingAnalysis.tsx';
import AiInsightCard from './shared/AiInsightCard.tsx';
const ManualWeighingPage: React.FC = () => {
const { manualWeighingData, cycleData, apiMode } = useAppContext();
const recordsByDate = useMemo(() => {
const groups: { [key: string]: (typeof manualWeighingData)[0] } = {};
manualWeighingData.forEach((record) => {
const group = groups[record.date] ?? (groups[record.date] = { ...record, sessions: [] });
group.sessions.push(...record.sessions);
});
return Object.values(groups).sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
}, [manualWeighingData]);
const [openAccordions, setOpenAccordions] = useState<Record<string, boolean>>(() => {
const first = recordsByDate[0];
return first ? { [first.date]: true } : {}; // Open first one by default
});
const [isAnalysisModalOpen, setIsAnalysisModalOpen] = useState(false);
const [selectedRecord, setSelectedRecord] = useState<{
record: ManualWeighingRecord;
dayOfCycle: number;
} | null>(null);
const handleToggle = (date: string) => {
setOpenAccordions((prev) => ({ ...prev, [date]: !prev[date] }));
};
const handleAnalyze = (record: ManualWeighingRecord, dayOfCycle: number) => {
setSelectedRecord({ record, dayOfCycle });
setIsAnalysisModalOpen(true);
};
const expandAll = () => {
const allOpen = recordsByDate.reduce(
(acc, record) => {
acc[record.date] = true;
return acc;
},
{} as Record<string, boolean>
);
setOpenAccordions(allOpen);
};
const collapseAll = () => {
setOpenAccordions({});
};
return (
<main className="px-4 sm:px-6 lg:px-8 pb-8">
<div className="flex items-center mb-6">
<FileInvoiceIcon className="text-3xl text-red-600 mr-4" />
<div>
<h1 className="text-3xl font-bold text-gray-800">Pencatatan Timbangan Manual</h1>
<p className="mt-1 text-gray-600">
Arsip dan analisis data timbangan manual untuk kalibrasi dan referensi.
</p>
</div>
</div>
{recordsByDate.length > 0 ? (
<>
{apiMode === 'DEMO' && (
<div className="mb-8">
<AiInsightCard title="AI Insight: Analisis Pertumbuhan Jangka Panjang">
<p>
Analisis tren pertumbuhan dari data manual menunjukkan Average Daily Gain (ADG)
yang sempat tertinggal dari target, terutama di fase grower. Namun, terjadi
percepatan pertumbuhan signifikan setelah hari ke-34. Meskipun demikian, bobot
rata-rata aktual (misal: <strong>1.972g</strong> pada hari ke-35) masih berada di
bawah target standar Ross 308 (<strong>2.237g</strong>). Ini mengindikasikan
adanya ketertinggalan kumulatif dari fase awal siklus. Rekomendasi: Fokus pada
optimalisasi kondisi brooding pada siklus berikutnya untuk memastikan target awal
tercapai.
</p>
</AiInsightCard>
</div>
)}
<ManualWeighingAnalysis records={recordsByDate} cycleData={cycleData} />
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center my-6 gap-4">
<h2 className="text-2xl font-bold text-gray-800">Rincian Data Harian</h2>
<div className="flex items-center space-x-2">
<button
onClick={expandAll}
className="text-sm text-gray-600 font-medium px-3 py-1.5 rounded-md hover:bg-gray-100 transition-colors"
>
Buka Semua
</button>
<button
onClick={collapseAll}
className="text-sm text-gray-600 font-medium px-3 py-1.5 rounded-md hover:bg-gray-100 transition-colors"
>
Tutup Semua
</button>
</div>
</div>
<div className="space-y-4">
{recordsByDate.map((record) => {
const recordDate = new Date(record.date);
const cycleStartDate = new Date(cycleData.startDate);
recordDate.setHours(0, 0, 0, 0);
cycleStartDate.setHours(0, 0, 0, 0);
const diffTime = recordDate.getTime() - cycleStartDate.getTime();
const dayOfCycle = Math.floor(diffTime / (1000 * 60 * 60 * 24)) + 14; // Data starts from day 14
const targetWeight = getTargetWeightForDay(dayOfCycle);
return (
<div
key={record.date}
className="bg-white rounded-xl shadow-sm border border-gray-200"
>
<div className="w-full flex justify-between items-center p-4 text-left">
<button
onClick={() => handleToggle(record.date)}
className="flex-grow text-left group"
>
<h2 className="text-lg font-bold text-gray-800 group-hover:text-red-700 transition-colors flex items-center flex-wrap gap-x-3 gap-y-2">
<span>
{new Date(record.date).toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
<span className="text-sm font-semibold text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
Hari ke-{dayOfCycle}
</span>
{record.aiAnalysisResult && (
<span
title="Analisa AI sudah disimpan"
className="text-green-700 bg-green-100 px-3 py-1 rounded-full text-xs font-bold flex items-center"
>
<i className="fa-solid fa-check-circle mr-1.5"></i>
TERANALISA
</span>
)}
</h2>
<p className="text-sm text-gray-500 mt-1">Sumber: {record.sourceDocument}</p>
</button>
<div className="flex items-center space-x-4 pl-4">
{record.aiAnalysisResult ? (
<button
onClick={() => handleAnalyze(record, dayOfCycle)}
className="flex items-center space-x-2 bg-green-50 text-green-700 px-3 py-2 rounded-lg hover:bg-green-100 transition duration-300 shadow-sm font-semibold text-sm"
title="Analisa AI sudah disimpan. Klik untuk melihat kembali."
>
<i className="fa-solid fa-check-circle"></i>
<span>Lihat Analisa</span>
</button>
) : (
<button
onClick={() => handleAnalyze(record, dayOfCycle)}
className="flex items-center space-x-2 bg-red-50 text-red-600 px-3 py-2 rounded-lg hover:bg-red-100 transition duration-300 shadow-sm font-semibold text-sm"
>
<BrainIcon />
<span>Analisa AI</span>
</button>
)}
<button
onClick={() => handleToggle(record.date)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100"
>
<i
className={`fa-solid fa-chevron-down text-gray-500 transform transition-transform ${openAccordions[record.date] ? 'rotate-180' : ''}`}
></i>
</button>
</div>
</div>
{openAccordions[record.date] && (
<div className="p-4 border-t border-gray-200">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-1">
<ManualWeighingStats record={record} dayOfCycle={dayOfCycle} />
</div>
<div className="lg:col-span-2 grid grid-cols-1 xl:grid-cols-2 gap-6">
{record.sessions.map((session) => (
<WeighingRecordCard
key={session.id}
session={session}
targetWeight={targetWeight}
/>
))}
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</>
) : (
<div className="text-center py-20 bg-white rounded-xl shadow-sm">
<i className="fa-solid fa-file-excel text-5xl text-gray-300"></i>
<p className="mt-4 text-lg font-semibold text-gray-600">Tidak ada data tersedia</p>
<p className="text-sm text-gray-500">Silakan unggah data timbangan manual.</p>
</div>
)}
{isAnalysisModalOpen && selectedRecord && (
<AiWeighingAnalysisModal
isOpen={isAnalysisModalOpen}
onClose={() => setIsAnalysisModalOpen(false)}
record={selectedRecord.record}
dayOfCycle={selectedRecord.dayOfCycle}
/>
)}
</main>
);
};
export default ManualWeighingPage;