commit
32a36cceff
444 files changed
+67186
No files matched your search
@@ -0,0 +1,358 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAppContext } from '../../context/AppContext.tsx';
|
||||
import { apiClient } from '../../services/apiService.ts';
|
||||
import { Kandang } from '../../types/counting.ts';
|
||||
|
||||
const KandangManagementPage: React.FC = () => {
|
||||
const { cycleData } = useAppContext();
|
||||
const [kandangs, setKandangs] = useState<Kandang[]>([]);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editDocInCount, setEditDocInCount] = useState('');
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [newDocInCount, setNewDocInCount] = useState('');
|
||||
|
||||
const loadKandangs = async () => {
|
||||
try {
|
||||
const data = await apiClient.getKandangsByCycle(cycleData.id);
|
||||
setKandangs(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load kandangs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadKandangs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cycleData]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!newName.trim()) return;
|
||||
try {
|
||||
const kandang = await apiClient.createKandang(newName.trim());
|
||||
const docCount = parseInt(newDocInCount, 10);
|
||||
if (!isNaN(docCount) && docCount > 0) {
|
||||
await apiClient.setKandangDocInCount(kandang.id, cycleData.id, docCount);
|
||||
}
|
||||
setNewName('');
|
||||
setNewDocInCount('');
|
||||
setIsAdding(false);
|
||||
loadKandangs();
|
||||
} catch (error) {
|
||||
console.error('Failed to create kandang:', error);
|
||||
alert('Gagal membuat kandang');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartEdit = (kandang: Kandang) => {
|
||||
setEditingId(kandang.id);
|
||||
setEditName(kandang.name);
|
||||
setEditDocInCount((kandang.docInCount || 0).toString());
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (editingId === null || !editName.trim()) return;
|
||||
try {
|
||||
await apiClient.updateKandang(editingId, editName.trim());
|
||||
const docCount = parseInt(editDocInCount, 10);
|
||||
if (!isNaN(docCount) && docCount >= 0) {
|
||||
await apiClient.setKandangDocInCount(editingId, cycleData.id, docCount);
|
||||
}
|
||||
setEditingId(null);
|
||||
loadKandangs();
|
||||
} catch (error) {
|
||||
console.error('Failed to update kandang:', error);
|
||||
alert('Gagal mengupdate kandang');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
if (!confirm(`Hapus "${name}"? Data mortalitas terkait juga akan terpengaruh.`)) return;
|
||||
try {
|
||||
await apiClient.deleteKandang(id);
|
||||
loadKandangs();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete kandang:', error);
|
||||
alert('Gagal menghapus kandang');
|
||||
}
|
||||
};
|
||||
|
||||
const totalDocInCount = kandangs.reduce((sum, k) => sum + (k.docInCount || 0), 0);
|
||||
|
||||
return (
|
||||
<main className="px-4 sm:px-6 lg:px-8 pb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800">Manajemen Kandang</h1>
|
||||
<p className="mt-1 text-gray-600">
|
||||
Kelola kandang dan populasi awal (DOC In) untuk setiap siklus produksi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current Cycle Indicator */}
|
||||
<div className="mb-6 bg-gradient-to-r from-red-50 to-orange-50 border-l-4 border-red-600 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-red-600 text-white rounded-full w-10 h-10 flex items-center justify-center">
|
||||
<i className="fa-solid fa-calendar-day"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Siklus Produksi Saat Ini</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
{cycleData.id}{' '}
|
||||
<span className="text-sm font-normal text-gray-600">
|
||||
(Hari {cycleData.currentDay} dari {cycleData.totalDays})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Card */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border-l-4 border-blue-500">
|
||||
<p className="text-sm text-gray-600 mb-1">Jumlah Kandang</p>
|
||||
<p className="text-2xl font-bold text-gray-800">{kandangs.length}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border-l-4 border-green-500">
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
Total DOC In <span className="text-xs">(Siklus {cycleData.id})</span>
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{totalDocInCount.toLocaleString()}{' '}
|
||||
<span className="text-sm font-normal text-gray-500">ekor</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border-l-4 border-orange-500">
|
||||
<p className="text-sm text-gray-600 mb-1">Rata-rata per Kandang</p>
|
||||
<p className="text-2xl font-bold text-orange-600">
|
||||
{kandangs.length > 0
|
||||
? Math.floor(totalDocInCount / kandangs.length).toLocaleString()
|
||||
: 0}{' '}
|
||||
<span className="text-sm font-normal text-gray-500">ekor</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add button */}
|
||||
{!isAdding && (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => setIsAdding(true)}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-plus mr-2"></i>
|
||||
Tambah Kandang
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add form */}
|
||||
{isAdding && (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Tambah Kandang Baru</h3>
|
||||
<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-1">Nama Kandang</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="Contoh: Kandang 2"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
DOC In (Siklus {cycleData.id})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newDocInCount}
|
||||
onChange={(e) => setNewDocInCount(e.target.value)}
|
||||
placeholder="Contoh: 10000"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<i className="fa-solid fa-info-circle mr-1"></i>
|
||||
Nilai ini khusus untuk siklus produksi ini
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-check mr-1"></i> Simpan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdding(false);
|
||||
setNewName('');
|
||||
setNewDocInCount('');
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kandang Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
#
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Nama Kandang
|
||||
</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
DOC (Siklus Aktif)
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Aksi
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{kandangs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-12 text-center text-gray-500">
|
||||
<i className="fa-solid fa-warehouse text-4xl mb-3 block text-gray-300"></i>
|
||||
Belum ada kandang. Tambahkan kandang baru untuk mulai.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
kandangs.map((kandang, index) => (
|
||||
<tr key={kandang.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 text-red-800 font-bold text-sm">
|
||||
{index + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{editingId === kandang.id ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="px-3 py-1 border border-blue-500 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span className="text-lg font-semibold text-gray-800">{kandang.name}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
{editingId === kandang.id ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editDocInCount}
|
||||
onChange={(e) => setEditDocInCount(e.target.value)}
|
||||
className="w-32 px-3 py-1 border border-blue-500 rounded-lg text-right focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-lg font-semibold text-green-600">
|
||||
{(kandang.docInCount || 0).toLocaleString()}
|
||||
<span className="text-xs ml-1 text-gray-500">ekor</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
{editingId === kandang.id ? (
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
onClick={handleSaveEdit}
|
||||
className="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="px-3 py-1 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors text-sm font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
onClick={() => handleStartEdit(kandang)}
|
||||
className="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-edit mr-1"></i> Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(kandang.id, kandang.name)}
|
||||
className="px-3 py-1 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm font-medium"
|
||||
>
|
||||
<i className="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
{kandangs.length > 0 && (
|
||||
<tfoot className="bg-gray-50 border-t-2 border-gray-300">
|
||||
<tr>
|
||||
<td className="px-6 py-4" colSpan={2}>
|
||||
<span className="font-bold text-gray-700">Total</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<span className="text-lg font-bold text-gray-800">
|
||||
{totalDocInCount.toLocaleString()}
|
||||
<span className="text-xs ml-1 text-gray-500">ekor</span>
|
||||
</span>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<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 Penting</h3>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>
|
||||
<i className="fa-solid fa-circle text-xs mr-2"></i>
|
||||
<strong>DOC In Count diatur per kandang untuk setiap siklus</strong> - ketika
|
||||
membuat siklus baru, Anda perlu mengatur ulang DOC In untuk semua kandang
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-circle text-xs mr-2"></i>Data mortalitas di "Manajemen
|
||||
Data" akan dipisahkan per kandang
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-circle text-xs mr-2"></i>Dashboard akan menampilkan data
|
||||
per kandang dan total gabungan
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-circle text-xs mr-2"></i>Menghapus kandang akan
|
||||
mempengaruhi semua data historis yang terkait
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default KandangManagementPage;
|
||||
Reference in new issue
Block a user