1166 lines
46 KiB
TypeScript
1166 lines
46 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useAppContext } from '../context/AppContext.tsx';
|
|
import { apiClient } from '../services/apiService.ts';
|
|
import {
|
|
Kandang,
|
|
FeedSackColumnConfig,
|
|
DEFAULT_FEED_SACK_COLUMNS,
|
|
FEED_SACK_COLUMN_COLORS,
|
|
} from '../types/counting.ts';
|
|
import {
|
|
normalizeFeedSackColumnConfig,
|
|
normalizeFeedSackColumnConfigs,
|
|
} from '../utils/feedSackMapping.ts';
|
|
|
|
const SettingsPage: React.FC = () => {
|
|
const { cycleData, cycles, onUpdateCycleDates, availableFeedSackCameras } = useAppContext();
|
|
const [kandangs, setKandangs] = useState<Kandang[]>([]);
|
|
|
|
// Cycle settings state
|
|
const [selectedCycleId, setSelectedCycleId] = useState<string>(cycleData?.id || '');
|
|
const [isEditingCycle, setIsEditingCycle] = useState(false);
|
|
const [cycleStartDate, setCycleStartDate] = useState<string>('');
|
|
const [cycleEndDate, setCycleEndDate] = useState<string>('');
|
|
const [cycleChickWeight, setCycleChickWeight] = useState<string>('');
|
|
const [cycleError, setCycleError] = useState<string>('');
|
|
|
|
// Kandang settings state
|
|
const [editingKandangId, setEditingKandangId] = useState<number | null>(null);
|
|
const [editingName, setEditingName] = useState('');
|
|
const [editingDocInCount, setEditingDocInCount] = useState('');
|
|
const [editingCageUuid, setEditingCageUuid] = useState('');
|
|
const [isAdding, setIsAdding] = useState(false);
|
|
const [newName, setNewName] = useState('');
|
|
const [newDocInCount, setNewDocInCount] = useState('');
|
|
|
|
// Column config settings state
|
|
const [columnConfig, setColumnConfig] =
|
|
useState<FeedSackColumnConfig[]>(DEFAULT_FEED_SACK_COLUMNS);
|
|
const [isEditingColumns, setIsEditingColumns] = useState(false);
|
|
const [editingColumns, setEditingColumns] = useState<FeedSackColumnConfig[]>([]);
|
|
const [columnError, setColumnError] = useState<string>('');
|
|
|
|
// UI state
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
// Helper to format Date to YYYY-MM-DD
|
|
const formatDateForInput = (date: Date): string => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// Load kandangs
|
|
const loadKandangs = async () => {
|
|
if (cycleData?.id) {
|
|
try {
|
|
const data = await apiClient.getKandangsByCycle(cycleData.id);
|
|
setKandangs(data);
|
|
} catch (error) {
|
|
console.error('Failed to load kandangs:', error);
|
|
setErrorMessage('Gagal memuat data kandang');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Load cycle data when selected cycle changes
|
|
useEffect(() => {
|
|
const cycle = cycles.find((c) => c.id === selectedCycleId);
|
|
if (cycle) {
|
|
setCycleStartDate(formatDateForInput(cycle.startDate));
|
|
const endDate = new Date(cycle.startDate);
|
|
endDate.setDate(endDate.getDate() + cycle.totalDays - 1);
|
|
setCycleEndDate(formatDateForInput(endDate));
|
|
setCycleChickWeight(cycle.chickInWeight?.toString() || '');
|
|
setCycleError('');
|
|
}
|
|
}, [selectedCycleId, cycles]);
|
|
|
|
// Initialize selected cycle
|
|
useEffect(() => {
|
|
if (cycleData?.id && !selectedCycleId) {
|
|
setSelectedCycleId(cycleData.id);
|
|
}
|
|
}, [cycleData?.id, selectedCycleId]);
|
|
|
|
useEffect(() => {
|
|
loadKandangs();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [cycleData?.id]);
|
|
|
|
// Calculate duration
|
|
const calculateDuration = (): number => {
|
|
if (!cycleStartDate || !cycleEndDate) return 0;
|
|
const start = new Date(cycleStartDate);
|
|
const end = new Date(cycleEndDate);
|
|
const diffMs = end.getTime() - start.getTime();
|
|
return Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
|
};
|
|
|
|
// Cycle settings handlers
|
|
const handleEditCycle = () => {
|
|
setIsEditingCycle(true);
|
|
setSuccessMessage(null);
|
|
setErrorMessage(null);
|
|
};
|
|
|
|
const handleCancelCycleEdit = () => {
|
|
setIsEditingCycle(false);
|
|
const cycle = cycles.find((c) => c.id === selectedCycleId);
|
|
if (cycle) {
|
|
setCycleStartDate(formatDateForInput(cycle.startDate));
|
|
const endDate = new Date(cycle.startDate);
|
|
endDate.setDate(endDate.getDate() + cycle.totalDays - 1);
|
|
setCycleEndDate(formatDateForInput(endDate));
|
|
setCycleChickWeight(cycle.chickInWeight?.toString() || '');
|
|
setCycleError('');
|
|
}
|
|
};
|
|
|
|
const handleSaveCycleSettings = () => {
|
|
const start = new Date(cycleStartDate);
|
|
const end = new Date(cycleEndDate);
|
|
const weight = cycleChickWeight ? parseInt(cycleChickWeight, 10) : undefined;
|
|
|
|
if (start > end) {
|
|
setCycleError('Tanggal mulai tidak boleh melebihi tanggal akhir');
|
|
return;
|
|
}
|
|
|
|
if (cycleChickWeight && (isNaN(weight!) || weight! <= 0)) {
|
|
setCycleError('Berat DOC harus berupa angka positif');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setCycleError('');
|
|
|
|
try {
|
|
onUpdateCycleDates(selectedCycleId, start, end, weight, undefined);
|
|
setSuccessMessage('Pengaturan cycle berhasil disimpan!');
|
|
setIsEditingCycle(false);
|
|
setTimeout(() => setSuccessMessage(null), 3000);
|
|
} catch {
|
|
setCycleError('Gagal menyimpan pengaturan cycle');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
// Kandang settings handlers
|
|
const handleEdit = (kandang: Kandang) => {
|
|
setEditingKandangId(kandang.id);
|
|
setEditingName(kandang.name);
|
|
setEditingDocInCount((kandang.docInCount || 0).toString());
|
|
setEditingCageUuid(kandang.cageUuid || '');
|
|
setSuccessMessage(null);
|
|
setErrorMessage(null);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setEditingKandangId(null);
|
|
setEditingName('');
|
|
setEditingDocInCount('');
|
|
setEditingCageUuid('');
|
|
setErrorMessage(null);
|
|
};
|
|
|
|
const handleSave = async (kandangId: number) => {
|
|
setIsSaving(true);
|
|
setSuccessMessage(null);
|
|
setErrorMessage(null);
|
|
|
|
try {
|
|
// Validate UUID format if provided
|
|
if (editingCageUuid.trim()) {
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
if (!uuidRegex.test(editingCageUuid.trim())) {
|
|
setErrorMessage('Format UUID tidak valid');
|
|
setIsSaving(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate kandang name
|
|
if (!editingName.trim()) {
|
|
setErrorMessage('Nama kandang tidak boleh kosong');
|
|
setIsSaving(false);
|
|
return;
|
|
}
|
|
|
|
// Update kandang name
|
|
await apiClient.updateKandang(kandangId, editingName.trim());
|
|
|
|
// Update DOC In Count
|
|
const docCount = parseInt(editingDocInCount, 10);
|
|
if (!isNaN(docCount) && docCount >= 0) {
|
|
await apiClient.setKandangDocInCount(kandangId, cycleData.id, docCount);
|
|
}
|
|
|
|
// Update cage UUID
|
|
await apiClient.updateKandangCageUuid(kandangId, editingCageUuid.trim() || null);
|
|
|
|
// Reload kandangs
|
|
await loadKandangs();
|
|
|
|
setEditingKandangId(null);
|
|
setEditingName('');
|
|
setEditingDocInCount('');
|
|
setEditingCageUuid('');
|
|
setSuccessMessage('Pengaturan kandang berhasil disimpan!');
|
|
|
|
// Clear success message after 3 seconds
|
|
setTimeout(() => setSuccessMessage(null), 3000);
|
|
} catch (error) {
|
|
console.error('Error saving kandang settings:', error);
|
|
setErrorMessage(error instanceof Error ? error.message : 'Gagal menyimpan konfigurasi');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleAdd = async () => {
|
|
if (!newName.trim()) {
|
|
setErrorMessage('Nama kandang tidak boleh kosong');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setSuccessMessage(null);
|
|
setErrorMessage(null);
|
|
|
|
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);
|
|
await loadKandangs();
|
|
setSuccessMessage('Kandang baru berhasil ditambahkan!');
|
|
setTimeout(() => setSuccessMessage(null), 3000);
|
|
} catch (error) {
|
|
console.error('Failed to create kandang:', error);
|
|
setErrorMessage(error instanceof Error ? error.message : 'Gagal membuat kandang');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: number, name: string) => {
|
|
if (
|
|
!confirm(
|
|
`Hapus kandang "${name}"? Data mortalitas dan pengaturan terkait juga akan terpengaruh.`
|
|
)
|
|
)
|
|
return;
|
|
|
|
setIsSaving(true);
|
|
try {
|
|
await apiClient.deleteKandang(id);
|
|
await loadKandangs();
|
|
setSuccessMessage('Kandang berhasil dihapus');
|
|
setTimeout(() => setSuccessMessage(null), 3000);
|
|
} catch (error) {
|
|
console.error('Failed to delete kandang:', error);
|
|
setErrorMessage(error instanceof Error ? error.message : 'Gagal menghapus kandang');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
// Load column config
|
|
const loadColumnConfig = async () => {
|
|
try {
|
|
const config = await apiClient.getFeedSackColumnConfig();
|
|
setColumnConfig(
|
|
config.length > 0 ? normalizeFeedSackColumnConfigs(config) : DEFAULT_FEED_SACK_COLUMNS
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to load column config:', error);
|
|
setColumnConfig(DEFAULT_FEED_SACK_COLUMNS);
|
|
}
|
|
};
|
|
|
|
// Load column config on mount
|
|
useEffect(() => {
|
|
loadColumnConfig();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Column config handlers
|
|
const handleEditColumns = () => {
|
|
setIsEditingColumns(true);
|
|
setEditingColumns(JSON.parse(JSON.stringify(columnConfig)));
|
|
setColumnError('');
|
|
setSuccessMessage(null);
|
|
setErrorMessage(null);
|
|
};
|
|
|
|
const handleCancelColumnsEdit = () => {
|
|
setIsEditingColumns(false);
|
|
setEditingColumns([]);
|
|
setColumnError('');
|
|
};
|
|
|
|
const handleAddColumn = () => {
|
|
const nextColor =
|
|
FEED_SACK_COLUMN_COLORS[editingColumns.length % FEED_SACK_COLUMN_COLORS.length] || '#22c55e';
|
|
const usedCameras = new Set(editingColumns.map((c) => c.cameraName));
|
|
const availableCamera =
|
|
availableFeedSackCameras.find((c) => !usedCameras.has(c)) ||
|
|
availableFeedSackCameras[0] ||
|
|
'';
|
|
|
|
const newColumn: FeedSackColumnConfig = {
|
|
id: `col-${Date.now()}`,
|
|
label: '',
|
|
cameraName: availableCamera,
|
|
color: nextColor,
|
|
type: 'usage',
|
|
kandangId: null,
|
|
};
|
|
|
|
setEditingColumns([...editingColumns, newColumn]);
|
|
};
|
|
|
|
const handleRemoveColumn = (id: string) => {
|
|
setEditingColumns(editingColumns.filter((c) => c.id !== id));
|
|
};
|
|
|
|
const handleUpdateColumn = (
|
|
id: string,
|
|
field: keyof FeedSackColumnConfig,
|
|
value: string | number | null
|
|
) => {
|
|
setEditingColumns(
|
|
editingColumns.map((c) =>
|
|
c.id === id ? ({ ...c, [field]: value } as FeedSackColumnConfig) : c
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleSaveColumns = async () => {
|
|
const validColumns = editingColumns
|
|
.filter((c) => c.cameraName && c.label.trim() && c.type)
|
|
.map(normalizeFeedSackColumnConfig);
|
|
|
|
if (validColumns.length !== editingColumns.length) {
|
|
setColumnError('Semua kolom harus lengkap (label, camera, dan type)');
|
|
return;
|
|
}
|
|
|
|
const hasInbound = validColumns.some((c) => c.type === 'inbound');
|
|
if (!hasInbound) {
|
|
setColumnError('Minimal harus ada satu kolom "Karung Masuk"');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setColumnError('');
|
|
|
|
try {
|
|
await apiClient.saveFeedSackColumnConfig(validColumns);
|
|
setColumnConfig(validColumns);
|
|
setIsEditingColumns(false);
|
|
setSuccessMessage('Konfigurasi kolom berhasil disimpan!');
|
|
setTimeout(() => setSuccessMessage(null), 3000);
|
|
} catch (error) {
|
|
console.error('Failed to save column config:', error);
|
|
setColumnError('Gagal menyimpan konfigurasi kolom');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const totalDocInCount = kandangs.reduce((sum, k) => sum + (k.docInCount || 0), 0);
|
|
|
|
return (
|
|
<main className="px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<i className="fa-solid fa-gear text-3xl text-red-600"></i>
|
|
<h1 className="text-3xl font-bold text-gray-900">Pengaturan</h1>
|
|
</div>
|
|
<p className="text-gray-600">Kelola pengaturan cycle, kandang, dan kolom hitung karung</p>
|
|
</div>
|
|
|
|
{/* Success/Error Messages */}
|
|
{successMessage && (
|
|
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3">
|
|
<i className="fa-solid fa-check-circle text-green-600 text-xl"></i>
|
|
<span className="text-green-800 font-medium">{successMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{errorMessage && (
|
|
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
|
|
<i className="fa-solid fa-exclamation-circle text-red-600 text-xl"></i>
|
|
<span className="text-red-800 font-medium">{errorMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Section A: Cycle Settings */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<i className="fa-solid fa-calendar-days text-2xl text-red-600"></i>
|
|
<h2 className="text-xl font-bold text-gray-900">Pengaturan Cycle</h2>
|
|
</div>
|
|
{!isEditingCycle && (
|
|
<button
|
|
onClick={handleEditCycle}
|
|
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<i className="fa-solid fa-edit"></i>
|
|
Edit
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cycle Selector */}
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Pilih Cycle</label>
|
|
<select
|
|
value={selectedCycleId}
|
|
onChange={(e) => {
|
|
setSelectedCycleId(e.target.value);
|
|
setIsEditingCycle(false);
|
|
}}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isEditingCycle}
|
|
>
|
|
{cycles.map((cycle) => (
|
|
<option key={cycle.id} value={cycle.id}>
|
|
{cycle.startDate.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' })} -{' '}
|
|
{cycle.status}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{isEditingCycle ? (
|
|
// Edit Mode
|
|
<>
|
|
{/* Date Fields */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tanggal Mulai
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={cycleStartDate}
|
|
onChange={(e) => setCycleStartDate(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tanggal Akhir
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={cycleEndDate}
|
|
onChange={(e) => setCycleEndDate(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Duration Display */}
|
|
<div className="mb-4 text-sm text-gray-600">
|
|
<span className="font-medium">Durasi:</span> {calculateDuration()} hari
|
|
</div>
|
|
|
|
{/* DOC Weight and Count */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Berat DOC</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
value={cycleChickWeight}
|
|
onChange={(e) => setCycleChickWeight(e.target.value)}
|
|
placeholder="Contoh: 40"
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
<span className="text-sm text-gray-600">gram</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
<i className="fa-solid fa-info-circle mr-1"></i>
|
|
Berat rata-rata DOC saat masuk (untuk perhitungan standar)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{cycleError && <div className="mb-4 text-sm text-red-600">{cycleError}</div>}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleSaveCycleSettings}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
{isSaving ? (
|
|
<>
|
|
<i className="fa-solid fa-spinner fa-spin"></i>
|
|
Menyimpan...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="fa-solid fa-save"></i>
|
|
Simpan
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleCancelCycleEdit}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50 text-sm font-medium"
|
|
>
|
|
Batal
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
// View Mode
|
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<p className="text-xs font-medium text-gray-500 mb-1">Tanggal Mulai:</p>
|
|
<p className="text-sm text-gray-800">
|
|
{cycleStartDate
|
|
? new Date(cycleStartDate).toLocaleDateString('id-ID', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
: '-'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-medium text-gray-500 mb-1">Tanggal Akhir:</p>
|
|
<p className="text-sm text-gray-800">
|
|
{cycleEndDate
|
|
? new Date(cycleEndDate).toLocaleDateString('id-ID', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
: '-'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<p className="text-xs font-medium text-gray-500 mb-1">Durasi:</p>
|
|
<p className="text-sm text-gray-800">{calculateDuration()} hari</p>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<p className="text-xs font-medium text-gray-500 mb-1">Berat DOC:</p>
|
|
<p className="text-sm text-gray-800">
|
|
{cycleChickWeight ? `${cycleChickWeight} gram` : '-'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Section B: Kandang Settings */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<i className="fa-solid fa-warehouse text-2xl text-red-600"></i>
|
|
<h2 className="text-xl font-bold text-gray-900">Pengaturan Kandang</h2>
|
|
</div>
|
|
|
|
{/* Summary Card */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<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-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<p className="text-sm text-gray-600 mb-1">Total Populasi Awal (Siklus Ini)</p>
|
|
<p className="text-2xl font-bold text-gray-800">{totalDocInCount.toLocaleString()}</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<p className="text-sm text-gray-600 mb-1">Kandang Terkonfigurasi</p>
|
|
<p className="text-2xl font-bold text-gray-800">
|
|
{kandangs.filter((k) => k.cageUuid).length} / {kandangs.length}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info Callout */}
|
|
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="flex gap-3">
|
|
<i className="fa-solid fa-info-circle text-blue-600 text-xl mt-0.5"></i>
|
|
<div className="text-sm text-blue-900">
|
|
<p className="font-medium mb-2">Informasi Penting:</p>
|
|
<ul className="list-disc list-inside space-y-1 text-blue-800">
|
|
<li>
|
|
<strong>Nama Kandang</strong> bersifat permanen untuk semua siklus
|
|
</li>
|
|
<li>
|
|
<strong>DOC In Count</strong> diatur per kandang untuk setiap siklus di bawah ini
|
|
</li>
|
|
<li>
|
|
<strong>Cage UUID</strong> digunakan untuk integrasi timbangan (API eksternal)
|
|
</li>
|
|
<li>
|
|
Format UUID:{' '}
|
|
<code className="bg-blue-100 px-1 rounded">
|
|
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
</code>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add New Kandang 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 flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
<i className="fa-solid fa-plus"></i>
|
|
Tambah Kandang Baru
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Add New Kandang Form */}
|
|
{isAdding && (
|
|
<div className="bg-gray-50 rounded-lg border border-gray-200 p-6 mb-6">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">Tambah Kandang Baru</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Nama Kandang</label>
|
|
<input
|
|
type="text"
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
placeholder="Contoh: Kandang 1, Kandang Atas"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
DOC In Count (Populasi Awal)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={newDocInCount}
|
|
onChange={(e) => setNewDocInCount(e.target.value)}
|
|
placeholder="Contoh: 25000"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleAdd}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
{isSaving ? (
|
|
<>
|
|
<i className="fa-solid fa-spinner fa-spin"></i>
|
|
Menyimpan...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="fa-solid fa-save"></i>
|
|
Simpan
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setIsAdding(false);
|
|
setNewName('');
|
|
setNewDocInCount('');
|
|
}}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50 text-sm font-medium"
|
|
>
|
|
Batal
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Kandang List */}
|
|
<div className="space-y-4">
|
|
{kandangs.map((kandang) => (
|
|
<div
|
|
key={kandang.id}
|
|
className="bg-gray-50 rounded-lg border border-gray-200 overflow-hidden"
|
|
>
|
|
<div className="p-6">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<i className="fa-solid fa-warehouse text-red-600"></i>
|
|
{kandang.name}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
ID: {kandang.id} • DOC In: {(kandang.docInCount || 0).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
{kandang.cageUuid ? (
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full bg-green-100 text-green-800 text-sm font-medium">
|
|
<i className="fa-solid fa-check-circle mr-1"></i>
|
|
Terkonfigurasi
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full bg-yellow-100 text-yellow-800 text-sm font-medium">
|
|
<i className="fa-solid fa-exclamation-triangle mr-1"></i>
|
|
Belum Lengkap
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{editingKandangId === kandang.id ? (
|
|
// Edit Mode
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nama Kandang
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={editingName}
|
|
onChange={(e) => setEditingName(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
DOC In Count (Populasi Awal)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={editingDocInCount}
|
|
onChange={(e) => setEditingDocInCount(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cage UUID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={editingCageUuid}
|
|
onChange={(e) => setEditingCageUuid(e.target.value)}
|
|
placeholder="Contoh: 9f556d2a-af5c-41b0-bcd0-6cea0dfa0fed"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500 font-mono text-sm"
|
|
disabled={isSaving}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
API:{' '}
|
|
<code className="text-gray-700">dashboard.cpsp.id/api/scales/cage/</code>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => handleSave(kandang.id)}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
{isSaving ? (
|
|
<>
|
|
<i className="fa-solid fa-spinner fa-spin"></i>
|
|
Menyimpan...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="fa-solid fa-save"></i>
|
|
Simpan
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleCancel}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50 text-sm font-medium"
|
|
>
|
|
Batal
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(kandang.id, kandang.name)}
|
|
disabled={isSaving}
|
|
className="ml-auto px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors disabled:opacity-50 text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<i className="fa-solid fa-trash"></i>
|
|
Hapus
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// View Mode
|
|
<div>
|
|
<div className="bg-white rounded-lg p-4 border border-gray-200">
|
|
{kandang.cageUuid ? (
|
|
<>
|
|
<p className="text-xs font-medium text-gray-500 mb-2">Cage UUID:</p>
|
|
<code className="text-sm font-mono text-gray-800 break-all block mb-3">
|
|
{kandang.cageUuid}
|
|
</code>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
<i className="fa-solid fa-exclamation-circle text-yellow-600 mr-2"></i>
|
|
Cage UUID belum dikonfigurasi
|
|
</p>
|
|
)}
|
|
<button
|
|
onClick={() => handleEdit(kandang)}
|
|
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<i className="fa-solid fa-edit"></i>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{kandangs.length === 0 && (
|
|
<div className="bg-gray-50 rounded-lg p-12 text-center border border-gray-200">
|
|
<i className="fa-solid fa-warehouse text-gray-300 text-6xl mb-4"></i>
|
|
<p className="text-gray-500 text-lg">Tidak ada kandang tersedia</p>
|
|
<button
|
|
onClick={() => setIsAdding(true)}
|
|
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors inline-flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
<i className="fa-solid fa-plus"></i>
|
|
Tambah Kandang Pertama
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Section C: Column Configuration */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<i className="fa-solid fa-table-columns text-2xl text-red-600"></i>
|
|
<h2 className="text-xl font-bold text-gray-900">Pengaturan Kolom Hitung Karung</h2>
|
|
</div>
|
|
{!isEditingColumns && (
|
|
<button
|
|
onClick={handleEditColumns}
|
|
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<i className="fa-solid fa-edit"></i>
|
|
Edit
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{isEditingColumns ? (
|
|
// Edit Mode
|
|
<>
|
|
{editingColumns.map((column, index) => (
|
|
<div
|
|
key={column.id}
|
|
className="bg-gray-50 rounded-lg border border-gray-200 p-6 mb-4"
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-800">Column {index + 1}</h3>
|
|
<button
|
|
onClick={() => handleRemoveColumn(column.id)}
|
|
disabled={isSaving}
|
|
className="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors disabled:opacity-50 text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<i className="fa-solid fa-trash"></i>
|
|
Hapus
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Label */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Label</label>
|
|
<input
|
|
type="text"
|
|
value={column.label}
|
|
onChange={(e) => handleUpdateColumn(column.id, 'label', e.target.value)}
|
|
placeholder="Contoh: Karung Masuk, Karung Dituang"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
|
|
{/* Camera */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Camera</label>
|
|
<select
|
|
value={column.cameraName}
|
|
onChange={(e) => handleUpdateColumn(column.id, 'cameraName', e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500"
|
|
disabled={isSaving}
|
|
>
|
|
{availableFeedSackCameras.map((camera) => (
|
|
<option key={camera} value={camera}>
|
|
{camera}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Kandang Mapping */}
|
|
{column.type === 'usage' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Mapping Kandang
|
|
</label>
|
|
<select
|
|
value={column.kandangId ?? ''}
|
|
onChange={(e) =>
|
|
handleUpdateColumn(
|
|
column.id,
|
|
'kandangId',
|
|
e.target.value ? Number(e.target.value) : null
|
|
)
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-red-500 focus:border-red-500 bg-white"
|
|
disabled={isSaving}
|
|
>
|
|
<option value="">-- Belum dipetakan --</option>
|
|
{kandangs.map((kandang) => (
|
|
<option key={kandang.id} value={kandang.id}>
|
|
{kandang.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Dipakai untuk menentukan kandang mana yang dihitung di home page.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Type */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Type</label>
|
|
<div className="flex gap-3 flex-wrap">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name={`type-${column.id}`}
|
|
value="inbound"
|
|
checked={column.type === 'inbound'}
|
|
onChange={(e) => handleUpdateColumn(column.id, 'type', e.target.value)}
|
|
className="text-red-600 focus:ring-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full bg-green-100 text-green-800 text-sm font-medium">
|
|
<i className="fa-solid fa-arrow-down-to-line mr-2"></i>
|
|
Karung Masuk
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name={`type-${column.id}`}
|
|
value="usage"
|
|
checked={column.type === 'usage'}
|
|
onChange={(e) => handleUpdateColumn(column.id, 'type', e.target.value)}
|
|
className="text-red-600 focus:ring-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full bg-orange-100 text-orange-800 text-sm font-medium">
|
|
<i className="fa-solid fa-utensils mr-2"></i>
|
|
Karung Dituang
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name={`type-${column.id}`}
|
|
value="outbound"
|
|
checked={column.type === 'outbound'}
|
|
onChange={(e) => handleUpdateColumn(column.id, 'type', e.target.value)}
|
|
className="text-red-600 focus:ring-red-500"
|
|
disabled={isSaving}
|
|
/>
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm font-medium">
|
|
<i className="fa-solid fa-arrow-up-from-line mr-2"></i>
|
|
Karung Keluar
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Color */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Color</label>
|
|
<div className="flex gap-2 flex-wrap">
|
|
{FEED_SACK_COLUMN_COLORS.map((color) => (
|
|
<button
|
|
key={color}
|
|
type="button"
|
|
onClick={() => handleUpdateColumn(column.id, 'color', color)}
|
|
disabled={isSaving}
|
|
className={`w-10 h-10 rounded-full transition-all ${
|
|
column.color === color
|
|
? 'ring-4 ring-red-500 ring-offset-2'
|
|
: 'hover:ring-2 hover:ring-gray-300'
|
|
}`}
|
|
style={{ backgroundColor: color }}
|
|
title={color}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Add Column Button */}
|
|
<button
|
|
onClick={handleAddColumn}
|
|
disabled={isSaving}
|
|
className="w-full px-4 py-3 border-2 border-dashed border-gray-300 rounded-lg hover:border-red-500 hover:bg-red-50 transition-colors text-gray-600 hover:text-red-600 font-medium flex items-center justify-center gap-2 disabled:opacity-50"
|
|
>
|
|
<i className="fa-solid fa-plus"></i>
|
|
Tambah Kolom Baru
|
|
</button>
|
|
|
|
{/* Error Message */}
|
|
{columnError && <div className="mt-4 text-sm text-red-600">{columnError}</div>}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
onClick={handleSaveColumns}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center gap-2 text-sm font-medium"
|
|
>
|
|
{isSaving ? (
|
|
<>
|
|
<i className="fa-solid fa-spinner fa-spin"></i>
|
|
Menyimpan...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="fa-solid fa-save"></i>
|
|
Simpan
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleCancelColumnsEdit}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50 text-sm font-medium"
|
|
>
|
|
Batal
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
// View Mode
|
|
<div>
|
|
{columnConfig.map((column, index) => (
|
|
<div
|
|
key={column.id}
|
|
className="bg-gray-50 rounded-lg border border-gray-200 p-4 mb-3"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<h3 className="text-base font-semibold text-gray-900 mb-2">
|
|
Column {index + 1}: {column.label}
|
|
</h3>
|
|
<p className="text-sm text-gray-600 mb-2">
|
|
<span className="font-medium">Camera:</span> {column.cameraName}
|
|
</p>
|
|
{column.type === 'usage' && (
|
|
<p className="text-sm text-gray-600 mb-2">
|
|
<span className="font-medium">Kandang:</span>{' '}
|
|
{column.kandangId
|
|
? kandangs.find((kandang) => kandang.id === column.kandangId)?.name ||
|
|
`ID ${column.kandangId}`
|
|
: 'Belum dipetakan'}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
|
column.type === 'inbound'
|
|
? 'bg-green-100 text-green-800'
|
|
: column.type === 'usage'
|
|
? 'bg-orange-100 text-orange-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}`}
|
|
>
|
|
{column.type === 'inbound' && (
|
|
<i className="fa-solid fa-arrow-down-to-line mr-2"></i>
|
|
)}
|
|
{column.type === 'usage' && <i className="fa-solid fa-utensils mr-2"></i>}
|
|
{column.type === 'outbound' && (
|
|
<i className="fa-solid fa-arrow-up-from-line mr-2"></i>
|
|
)}
|
|
{column.type === 'inbound'
|
|
? 'Karung Masuk'
|
|
: column.type === 'usage'
|
|
? 'Karung Dituang'
|
|
: 'Karung Keluar'}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-600">Color:</span>
|
|
<div
|
|
className="w-6 h-6 rounded-full border-2 border-gray-300"
|
|
style={{ backgroundColor: column.color }}
|
|
title={column.color}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{columnConfig.length === 0 && (
|
|
<div className="bg-gray-50 rounded-lg p-12 text-center border border-gray-200">
|
|
<i className="fa-solid fa-table-columns text-gray-300 text-6xl mb-4"></i>
|
|
<p className="text-gray-500 text-lg">Tidak ada kolom terkonfigurasi</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
);
|
|
};
|
|
|
|
export default SettingsPage;
|