updating cycle close request feature to get approval from national dashboard instead auto from system

This commit is contained in:
Alberto-Audrix committed 2026-09-09 14:18:39 +07:00
1 parent 3a868cf1db
commit e4dbfd0252
31 files changed
+1322 -182

No files matched your search

+49 -42
View File
@@ -2,8 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import { api, ApiError, errorMessage } from '../services/apiClient.ts';
import type { AIInsight, ChickenWeight, LatestAvg } from '../types/api.ts';
import { useFarm } from '../context/FarmContext.tsx';
import { Banner, EmptyFarm, Spinner } from './ui/Feedback.tsx';
import { formatDateId, formatNumber } from '../utils/format.ts';
import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx';
import { formatDateId, formatDateRange, formatNumber, resolveCycleStatus } from '../utils/format.ts';
import { formatWeight } from '../utils/weight.ts';
import WeightStats from './weight/WeightStats.tsx';
import DailyWeightProgressionChart from './weight/DailyWeightProgressionChart.tsx';
@@ -11,15 +11,18 @@ import DailyWeightGainChart from './weight/DailyWeightGainChart.tsx';
import DailyDetailPanel from './weight/DailyDetailPanel.tsx';
const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
const { selectedCycle } = useFarm();
const { selectedCycle, selectedSite, selectedKandang } = useFarm();
const [rows, setRows] = useState<ChickenWeight[]>([]);
const [latest, setLatest] = useState<LatestAvg | null>(null);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [syncing, setSyncing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState('');
const isClosedCycle = !selectedCycle || resolveCycleStatus(selectedCycle) !== 'active';
const reload = async (cycleId: number) => {
setError(null);
try {
@@ -51,7 +54,6 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
let cancelled = false;
(async () => {
setLoading(true);
setRefreshing(false);
try {
await reload(selectedCycle.id);
} finally {
@@ -78,15 +80,19 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
}
}, [rows, chartRows, selectedDate]);
const handleRefresh = async () => {
if (!selectedCycle) return;
setRefreshing(true);
const handleSync = async () => {
if (!selectedCycle || isClosedCycle) return;
setSyncing(true);
setError(null);
setNotice(null);
try {
await api.weights.sync({ cycle_id: selectedCycle.id });
setNotice('Sinkron berat edge berhasil.');
await reload(selectedCycle.id);
} catch {
// error state is set inside reload
} catch (err) {
setError(errorMessage(err));
} finally {
setRefreshing(false);
setSyncing(false);
}
};
@@ -130,41 +136,42 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
return (
<main className="pb-8">
{/* Header */}
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div className="flex items-center">
<div className="bg-red-600 text-white p-3 rounded-lg mr-4">
<i className="fa-solid fa-chart-line text-2xl sm:text-3xl"></i>
</div>
<div>
<h1 className="text-2xl lg:text-3xl font-bold text-gray-800">Dashboard Berat Ayam</h1>
<p className="mt-1 text-gray-600">
Analisis pertumbuhan dan kesejahteraan bobot ayam secara real-time.
</p>
{!loading && latest?.date && (
<p className="mt-1 text-sm text-gray-500">
<i className="fa-solid fa-clock mr-1"></i>
Terakhir diperbarui: {formatDateId(latest.date)}
</p>
<PageHeader
title="Dashboard Berat Ayam"
icon={<i className="fa-solid fa-chart-line" />}
subtitle={`Analisis pertumbuhan bobot ayam secara real-time · ${selectedSite?.site_name ?? ''} · ${selectedKandang?.kandang_name ?? ''} · Siklus ${formatDateRange(selectedCycle.start_date, selectedCycle.end_date)}`}
actions={
<button
type="button"
onClick={handleSync}
disabled={syncing || isClosedCycle}
className="bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-semibold disabled:opacity-60 hover:bg-red-700 transition-colors"
>
{syncing ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
Sync...
</>
) : (
<>
<i className="fa-solid fa-arrows-rotate mr-2"></i>
Sync
</>
)}
</div>
</div>
<button
onClick={handleRefresh}
disabled={refreshing || loading}
className="flex items-center space-x-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
title="Refresh data manual"
>
<i className={`fa-solid fa-refresh ${refreshing || loading ? 'fa-spin' : ''}`}></i>
<span>{refreshing || loading ? 'Memuat...' : 'Refresh'}</span>
</button>
</div>
</button>
}
/>
{error && (
<div className="mb-4">
<Banner kind="error">{error}</Banner>
</div>
)}
{notice && (
<div className="mb-4">
<Banner kind="success">{notice}</Banner>
</div>
)}
{loading && rows.length === 0 ? (
<Spinner />
@@ -297,9 +304,9 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<div>
<h3 className="font-semibold text-blue-900 mb-2">Informasi Pembaruan Data</h3>
<ul className="text-sm text-blue-800 space-y-1">
<li>• Klik tombol "Refresh" untuk memperbarui data secara manual</li>
<li>• Grafik menampilkan data dari awal siklus hingga hari ini</li>
<li>• Target berat menggunakan standar performa Ross 308</li>
<li>• Klik "Sync" untuk menarik prediksi berat dari edge vision</li>
<li>• Hari 0 memakai berat DOC dan uniformity 0% (bukan data edge)</li>
<li>• Target berat menggunakan standar performa berdasarkan DOC</li>
</ul>
</div>
</div>