Files
dashboard-cpsp/components/feedSack/InitialBalanceCard.tsx
T
2026-08-26 13:46:05 +07:00

165 lines
6.3 KiB
TypeScript

import React, { 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';
interface InitialBalanceCardProps {
cycle: Cycle;
onSaved: () => void;
}
const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved }) => {
const [balanceDate, setBalanceDate] = useState(cycle.start_date);
const [manualEntry, setManualEntry] = useState<string>(
cycle.feed_initial_balance > 0 ? String(cycle.feed_initial_balance) : ''
);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<InitialBalanceResult | null>(null);
const canSave = useMemo(
() => balanceDate !== '' && manualEntry !== '' && Number(manualEntry) >= 0 && !saving,
[balanceDate, manualEntry, saving]
);
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),
});
setResult(saved);
onSaved();
} catch (err) {
setError(errorMessage(err));
} finally {
setSaving(false);
}
};
const accuracy = result?.accuracy;
const accuracyLabel = accuracy == null ? 'N/A' : `${formatNumber(accuracy, 1)}%`;
const accuracyColor =
accuracy == null
? 'text-gray-500'
: accuracy >= 95
? 'text-green-600'
: accuracy >= 80
? 'text-yellow-600'
: 'text-red-600';
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.
</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">
<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>
{cycle.feed_initial_balance > 0 && result == 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>
{result && (
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50/50 p-4">
<p className="text-sm font-semibold text-gray-800 mb-3">Perbandingan {result.date}</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-center">
<div className="rounded-lg bg-white border border-gray-200 p-3">
<p className="text-xs font-semibold text-blue-700">Manual</p>
<p className="text-2xl font-bold text-gray-800">
{formatNumber(result.manual)} karung
</p>
</div>
<div className="rounded-lg bg-white border border-gray-200 p-3">
<p className="text-xs font-semibold text-purple-700">IOT</p>
<p className="text-2xl font-bold text-gray-800">
{result.iot == null ? 'N/A' : `${formatNumber(result.iot)} karung`}
</p>
</div>
<div className="rounded-lg bg-white border border-gray-200 p-3">
<p className="text-xs font-semibold text-gray-600">Akurasi</p>
<p className={`text-2xl font-bold ${accuracyColor}`}>{accuracyLabel}</p>
</div>
</div>
{result.sync_error && (
<p className="mt-3 text-xs text-yellow-700 bg-yellow-50 border border-yellow-200 rounded-lg p-2">
Gagal menarik data IoT dari karung-web-admin: {result.sync_error}
</p>
)}
</div>
)}
</div>
</div>
);
};
export default InitialBalanceCard;