179 lines
6.7 KiB
TypeScript
179 lines
6.7 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { api, errorMessage } from '../../services/apiClient.ts';
|
|
import type { Cycle, InitialBalanceResult } from '../../types/api.ts';
|
|
import { formatNumber } from '../../utils/format.ts';
|
|
import InitialBalanceComparison from './InitialBalanceComparison.tsx';
|
|
|
|
interface InitialBalanceCardProps {
|
|
cycle: Cycle;
|
|
onSaved: (result: InitialBalanceResult) => void;
|
|
}
|
|
|
|
const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved }) => {
|
|
const savedBalanceDate = cycle.feed_initial_balance_date ?? cycle.start_date;
|
|
const [balanceDate, setBalanceDate] = useState(savedBalanceDate);
|
|
const [manualEntry, setManualEntry] = useState<string>(
|
|
cycle.feed_initial_balance > 0 ? String(cycle.feed_initial_balance) : ''
|
|
);
|
|
const [saving, setSaving] = useState(false);
|
|
const [comparing, setComparing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [comparison, setComparison] = useState<InitialBalanceResult | null>(null);
|
|
|
|
useEffect(() => {
|
|
setBalanceDate(cycle.feed_initial_balance_date ?? cycle.start_date);
|
|
setManualEntry(cycle.feed_initial_balance > 0 ? String(cycle.feed_initial_balance) : '');
|
|
setComparison(null);
|
|
setError(null);
|
|
}, [cycle.id, cycle.feed_initial_balance, cycle.feed_initial_balance_date, cycle.start_date]);
|
|
|
|
const manualValue = manualEntry === '' ? null : Number(manualEntry);
|
|
|
|
const canSave = useMemo(
|
|
() => balanceDate !== '' && manualEntry !== '' && Number(manualEntry) >= 0 && !saving,
|
|
[balanceDate, manualEntry, saving]
|
|
);
|
|
|
|
const loadComparison = useCallback(async () => {
|
|
if (balanceDate === '' || manualValue == null || Number.isNaN(manualValue) || manualValue < 0) {
|
|
setComparison(null);
|
|
return;
|
|
}
|
|
|
|
setComparing(true);
|
|
try {
|
|
const compared = await api.cycles.initialBalanceCompare(cycle.id, {
|
|
date: balanceDate,
|
|
manual: manualValue,
|
|
});
|
|
setComparison(compared);
|
|
} catch (err) {
|
|
setComparison(null);
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setComparing(false);
|
|
}
|
|
}, [balanceDate, cycle.id, manualValue]);
|
|
|
|
useEffect(() => {
|
|
if (manualValue == null || manualValue < 0) {
|
|
setComparison(null);
|
|
return;
|
|
}
|
|
void loadComparison();
|
|
}, [loadComparison, manualValue]);
|
|
|
|
const handleSave = async () => {
|
|
if (!canSave) return;
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
const saved = await api.cycles.initialBalance(cycle.id, {
|
|
date: balanceDate,
|
|
feed_in_manual: Number(manualEntry),
|
|
});
|
|
setComparison(saved);
|
|
onSaved(saved);
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl shadow-sm border-2 border-blue-200">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="bg-blue-500 text-white p-3 rounded-lg">
|
|
<i className="fa-solid fa-boxes-packing text-2xl"></i>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800">Saldo Awal</h2>
|
|
<p className="text-sm text-gray-600">
|
|
Masukkan saldo awal karung pakan pada tanggal tertentu, lalu bandingkan dengan deteksi
|
|
IoT dari karung-web-admin.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg p-6 border border-blue-200">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label
|
|
htmlFor="initial-balance-date"
|
|
className="text-sm font-semibold text-gray-700 block mb-1"
|
|
>
|
|
Tanggal
|
|
</label>
|
|
<input
|
|
id="initial-balance-date"
|
|
type="date"
|
|
value={balanceDate}
|
|
onChange={(event) => setBalanceDate(event.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="initial-balance-manual"
|
|
className="text-sm font-semibold text-gray-700 block mb-1"
|
|
>
|
|
Feed In Manual (karung)
|
|
</label>
|
|
<input
|
|
id="initial-balance-manual"
|
|
type="number"
|
|
min={0}
|
|
value={manualEntry}
|
|
onChange={(event) => setManualEntry(event.target.value)}
|
|
placeholder="Contoh: 120"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="mt-3 text-sm font-semibold text-red-700 bg-red-50 border border-red-200 rounded-lg p-3">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={!canSave}
|
|
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
>
|
|
<i className={`fa-solid fa-floppy-disk ${saving ? 'fa-spin' : ''}`} />
|
|
<span>{saving ? 'Menyimpan...' : 'Simpan Saldo Awal'}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => void loadComparison()}
|
|
disabled={manualValue == null || manualValue < 0 || comparing}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-blue-300 bg-white px-4 py-2 text-sm font-semibold text-blue-700 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:text-gray-400"
|
|
>
|
|
<i className={`fa-solid fa-satellite-dish ${comparing ? 'fa-spin' : ''}`} />
|
|
<span>{comparing ? 'Memuat IoT...' : 'Refresh IoT'}</span>
|
|
</button>
|
|
</div>
|
|
{cycle.feed_initial_balance > 0 && comparison == null && (
|
|
<p className="text-sm text-gray-600">
|
|
Saldo awal tersimpan:{' '}
|
|
<strong className="text-blue-700">
|
|
{formatNumber(cycle.feed_initial_balance)} karung
|
|
</strong>
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{comparison && <InitialBalanceComparison result={comparison} loading={comparing} />}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InitialBalanceCard;
|