update sync data from site dashboard and testing closing cycles

This commit is contained in:
Alberto-Audrix committed 2026-09-15 16:52:41 +07:00
1 parent 36639d8c38
commit f49e72592c
42 files changed
+1966 -688

No files matched your search

@@ -93,7 +93,7 @@ const FeedDailySummaryTable: React.FC<FeedDailySummaryTableProps> = ({
<i className="fa-solid fa-table text-5xl text-gray-300 mb-3"></i>
<p className="font-medium text-gray-600">Tidak ada data</p>
<p className="text-sm text-gray-500 mt-1">
Belum ada data karung IOT untuk siklus ini. Sinkronkan lewat tombol Sync hari ini.
Belum ada data karung IOT untuk siklus ini. Gunakan Sync di header untuk menarik data dari site.
</p>
</div>
</div>
+1 -1
View File
@@ -109,7 +109,7 @@ const FeedSaldoCard: React.FC<FeedSaldoCardProps> = ({
<i className="fa-solid fa-triangle-exclamation text-yellow-600 mt-0.5"></i>
<p className="text-xs text-yellow-800">
<strong>Peringatan:</strong> Saldo negatif menunjukkan karung dituang lebih banyak dari
karung masuk. Periksa kembali data sinkronisasi karung-web-admin.
karung masuk. Pastikan site sudah disinkronkan dari header.
</p>
</div>
)}
+21 -102
View File
@@ -1,7 +1,5 @@
import React, { useCallback, useEffect, 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 React from 'react';
import type { Cycle } from '../../types/api.ts';
import InitialBalanceComparison from './InitialBalanceComparison.tsx';
interface InitialBalanceCardProps {
@@ -9,53 +7,9 @@ interface InitialBalanceCardProps {
}
const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle }) => {
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 [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 loadComparison = useCallback(async () => {
if (balanceDate === '' || manualValue == null || Number.isNaN(manualValue) || manualValue < 0) {
setComparison(null);
return;
}
setComparing(true);
setError(null);
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 hasBalance =
cycle.feed_initial_balance > 0 || Boolean(cycle.feed_initial_balance_date);
const balanceDate = cycle.feed_initial_balance_date ?? cycle.start_date;
return (
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl shadow-sm border-2 border-blue-200">
@@ -66,64 +20,29 @@ const InitialBalanceCard: React.FC<InitialBalanceCardProps> = ({ cycle }) => {
<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.
Saldo awal karung pakan dari site dashboard (mirror sync).
</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"
{!hasBalance ? (
<p className="text-sm text-gray-600">Belum ada saldo awal tersimpan untuk siklus ini.</p>
) : (
<>
<p className="text-sm text-gray-600 mb-1">
Tanggal:{' '}
<strong className="text-gray-800">{balanceDate}</strong>
</p>
<InitialBalanceComparison
date={balanceDate}
manual={cycle.feed_initial_balance}
iot={cycle.feed_initial_balance_iot ?? null}
accuracy={cycle.feed_initial_balance_accuracy ?? null}
syncError={cycle.feed_initial_balance_sync_error ?? null}
/>
</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>
</>
)}
{cycle.feed_initial_balance > 0 && comparison == null && (
<p className="mt-4 text-sm text-gray-600">
Saldo awal tersimpan:{' '}
<strong className="text-blue-700">
{formatNumber(cycle.feed_initial_balance)} karung
</strong>
</p>
)}
{comparison && <InitialBalanceComparison result={comparison} loading={comparing} />}
</div>
</div>
);
@@ -1,17 +1,23 @@
import React from 'react';
import type { InitialBalanceResult } from '../../types/api.ts';
import { formatNumber } from '../../utils/format.ts';
interface InitialBalanceComparisonProps {
result: InitialBalanceResult;
date: string;
manual: number;
iot: number | null;
accuracy: number | null;
syncError?: string | null;
loading?: boolean;
}
const InitialBalanceComparison: React.FC<InitialBalanceComparisonProps> = ({
result,
date,
manual,
iot,
accuracy,
syncError = null,
loading = false,
}) => {
const accuracy = result.accuracy;
const accuracyLabel = accuracy == null ? 'N/A' : `${formatNumber(accuracy, 1)}%`;
const accuracyColor =
accuracy == null
@@ -25,18 +31,18 @@ const InitialBalanceComparison: React.FC<InitialBalanceComparisonProps> = ({
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...' : ''}
Perbandingan {date}
{loading ? ' · memuat...' : ''}
</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>
<p className="text-2xl font-bold text-gray-800">{formatNumber(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`}
{iot == null ? 'N/A' : `${formatNumber(iot)} karung`}
</p>
</div>
<div className="rounded-lg bg-white border border-gray-200 p-3">
@@ -44,9 +50,9 @@ const InitialBalanceComparison: React.FC<InitialBalanceComparisonProps> = ({
<p className={`text-2xl font-bold ${accuracyColor}`}>{accuracyLabel}</p>
</div>
</div>
{result.sync_error && (
{syncError && (
<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}
Sinkron IoT di site: {syncError}
</p>
)}
</div>