Files
dashboard/components/SettingsPage.tsx
T
Alberto-Audrix 32a36cceff
CI / lint-and-test (push) Canceled after 0s
first commit
2026-07-28 08:55:05 +07:00

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;