update add feed initial balance on cycle and fix its feature
This commit is contained in:
1 parent
91444faa31
commit
7a9d600aa6
16 files changed
+439
-88
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
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;
|
||||
@@ -9,19 +10,59 @@ interface InitialBalanceCardProps {
|
||||
}
|
||||
|
||||
const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved }) => {
|
||||
const [balanceDate, setBalanceDate] = useState(cycle.start_date);
|
||||
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 [result, setResult] = useState<InitialBalanceResult | 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);
|
||||
@@ -31,7 +72,7 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved
|
||||
date: balanceDate,
|
||||
feed_in_manual: Number(manualEntry),
|
||||
});
|
||||
setResult(saved);
|
||||
setComparison(saved);
|
||||
onSaved(saved);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
@@ -40,17 +81,6 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
@@ -61,7 +91,7 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved
|
||||
<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.
|
||||
IoT dari karung-web-admin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,16 +139,27 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved
|
||||
)}
|
||||
|
||||
<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 && (
|
||||
<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">
|
||||
@@ -128,34 +169,7 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle, onSaved
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
{comparison && <InitialBalanceComparison result={comparison} loading={comparing} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import type { InitialBalanceResult } from '../../types/api.ts';
|
||||
import { formatNumber } from '../../utils/format.ts';
|
||||
|
||||
interface InitialBalanceComparisonProps {
|
||||
result: InitialBalanceResult;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const InitialBalanceComparison: React.FC<InitialBalanceComparisonProps> = ({
|
||||
result,
|
||||
loading = 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="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}
|
||||
{loading ? ' · memuat IoT...' : ''}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitialBalanceComparison;
|
||||
Reference in new issue
Block a user