first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
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;