UPDATE
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
@@ -25,98 +24,72 @@ import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPla
|
||||
import RelatedPlanSelector, {
|
||||
type RelatedPlanItem,
|
||||
} from "@views/dashboards/farm/planSelector/RelatedPlanSelector";
|
||||
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||
import {
|
||||
fertilizationPlanService,
|
||||
type FertilizationPlanDetail,
|
||||
type FertilizationPlanListItem,
|
||||
} from "@/libs/api/services/fertilizationPlanService";
|
||||
import { irrigationPlanService } from "@/libs/api/services/irrigationPlanService";
|
||||
|
||||
import CustomTextField from "@core/components/mui/TextField";
|
||||
|
||||
type FertilizationPlanRow = {
|
||||
id: number;
|
||||
id: string;
|
||||
planName: string;
|
||||
finalProduct: string;
|
||||
harvestTime: string;
|
||||
outputTon: number;
|
||||
fertilizerType: string;
|
||||
status: "active" | "draft";
|
||||
sourceLabel: string;
|
||||
growthStage: string;
|
||||
};
|
||||
|
||||
const mockFertilizationPlans: FertilizationPlanRow[] = [
|
||||
{
|
||||
id: 1,
|
||||
planName: "برنامه کوددهی گوجه فرنگی گلخانهای",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۱۲",
|
||||
outputTon: 52,
|
||||
fertilizerType: "NPK 20-20-20",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
planName: "برنامه کوددهی فلفل دلمهای",
|
||||
finalProduct: "فلفل دلمهای رنگی",
|
||||
harvestTime: "۱۴۰۴/۰۵/۲۰",
|
||||
outputTon: 34,
|
||||
fertilizerType: "نیترات کلسیم",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
planName: "برنامه کوددهی خیار گلخانهای",
|
||||
finalProduct: "خیار ممتاز صادراتی",
|
||||
harvestTime: "۱۴۰۴/۰۴/۲۸",
|
||||
outputTon: 41,
|
||||
fertilizerType: "سولفات پتاسیم",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
planName: "برنامه کوددهی هندوانه",
|
||||
finalProduct: "هندوانه شیرین بازارپسند",
|
||||
harvestTime: "۱۴۰۴/۰۵/۲۹",
|
||||
outputTon: 57,
|
||||
fertilizerType: "اوره + هیومیک اسید",
|
||||
status: "draft",
|
||||
},
|
||||
];
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const relatedIrrigationPlans: RelatedPlanItem[] = [
|
||||
{
|
||||
id: 301,
|
||||
title: "آبیاری قطرهای صبحگاهی",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۰۸",
|
||||
outputTon: 45,
|
||||
methodLabel: "روش آبیاری: قطرهای",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
title: "آبیاری تنظیمشده مرحله گلدهی",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۱۰",
|
||||
outputTon: 49,
|
||||
methodLabel: "روش آبیاری: تیپ",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
title: "آبیاری تقویتی پیش از برداشت",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۱۴",
|
||||
outputTon: 53,
|
||||
methodLabel: "روش آبیاری: بارانی سبک",
|
||||
status: "draft",
|
||||
},
|
||||
];
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return "—";
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const mapFertilizationPlanRow = (
|
||||
plan: FertilizationPlanListItem,
|
||||
detail?: FertilizationPlanDetail | null,
|
||||
): FertilizationPlanRow => ({
|
||||
id: plan.plan_uuid,
|
||||
planName: plan.title,
|
||||
finalProduct: plan.plant_name || plan.crop_id || "—",
|
||||
harvestTime: formatDate(plan.created_at),
|
||||
outputTon: 0,
|
||||
fertilizerType: detail?.plan_payload?.items?.[0]?.name || plan.source_label || "—",
|
||||
status: plan.is_active ? "active" : "draft",
|
||||
sourceLabel: plan.source_label || "—",
|
||||
growthStage: plan.growth_stage || "—",
|
||||
});
|
||||
|
||||
const FertilizationPlanPage = () => {
|
||||
const router = useRouter();
|
||||
const [plans, setPlans] = useState(mockFertilizationPlans);
|
||||
const { getFarmUuid } = useFarmHub();
|
||||
const [plans, setPlans] = useState<FertilizationPlanRow[]>([]);
|
||||
const [minOutputTon, setMinOutputTon] = useState("0");
|
||||
const [selectedPlan, setSelectedPlan] = useState<FertilizationPlanRow | null>(
|
||||
mockFertilizationPlans[0],
|
||||
);
|
||||
const [selectedPlan, setSelectedPlan] = useState<FertilizationPlanRow | null>(null);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | null>(
|
||||
relatedIrrigationPlans[0]?.id ?? null,
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailsLoading, setDetailsLoading] = useState(false);
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [relatedPlans, setRelatedPlans] = useState<RelatedPlanItem[]>([]);
|
||||
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
@@ -127,51 +100,134 @@ const FertilizationPlanPage = () => {
|
||||
.sort((firstPlan, secondPlan) => secondPlan.outputTon - firstPlan.outputTon);
|
||||
}, [minOutputTon, plans]);
|
||||
|
||||
const handleActivate = (planId: number) => {
|
||||
const loadPlans = useCallback(async () => {
|
||||
const farmUuid = getFarmUuid();
|
||||
|
||||
if (!farmUuid) {
|
||||
setError("ابتدا مزرعه فعال را انتخاب کنید.");
|
||||
setPlans([]);
|
||||
setSelectedPlan(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fertilizationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
|
||||
const mappedPlans = response.data.map((plan) => mapFertilizationPlanRow(plan));
|
||||
|
||||
setPlans(mappedPlans);
|
||||
setSelectedPlan((current) =>
|
||||
mappedPlans.find((plan) => plan.id === current?.id) ?? mappedPlans[0] ?? null,
|
||||
);
|
||||
} catch (loadError) {
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "دریافت لیست برنامههای کوددهی انجام نشد.",
|
||||
);
|
||||
setPlans([]);
|
||||
setSelectedPlan(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [getFarmUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
}, [loadPlans]);
|
||||
|
||||
const loadRelatedPlans = useCallback(async () => {
|
||||
const farmUuid = getFarmUuid();
|
||||
|
||||
if (!farmUuid) {
|
||||
setRelatedPlans([]);
|
||||
setSelectedRelatedPlanId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await irrigationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
|
||||
const items: RelatedPlanItem[] = response.data.map((plan) => ({
|
||||
id: plan.plan_uuid,
|
||||
title: plan.title,
|
||||
finalProduct: plan.plant_name || plan.crop_id || "—",
|
||||
harvestTime: formatDate(plan.created_at),
|
||||
outputTon: 0,
|
||||
methodLabel: `منبع: ${plan.source_label || "—"}`,
|
||||
status: plan.is_active ? "active" : "draft",
|
||||
}));
|
||||
|
||||
setRelatedPlans(items);
|
||||
setSelectedRelatedPlanId(items[0]?.id ?? null);
|
||||
}, [getFarmUuid]);
|
||||
|
||||
const handleActivate = async (planId: string) => {
|
||||
setActionLoadingId(planId);
|
||||
|
||||
try {
|
||||
await fertilizationPlanService.updatePlanStatus(planId, true);
|
||||
setPlans((currentPlans) =>
|
||||
currentPlans.map((plan) => ({
|
||||
...plan,
|
||||
status: plan.id === planId ? "active" : "draft",
|
||||
})),
|
||||
);
|
||||
|
||||
const activePlan = plans.find((plan) => plan.id === planId);
|
||||
|
||||
if (activePlan) {
|
||||
setSelectedPlan({ ...activePlan, status: "active" });
|
||||
setSelectedPlan((currentPlan) =>
|
||||
currentPlan && currentPlan.id === planId
|
||||
? { ...currentPlan, status: "active" }
|
||||
: currentPlan,
|
||||
);
|
||||
} catch (updateError) {
|
||||
setError(
|
||||
updateError instanceof Error
|
||||
? updateError.message
|
||||
: "تغییر وضعیت برنامه کوددهی انجام نشد.",
|
||||
);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (planId: number) => {
|
||||
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||
const handleDelete = async (planId: string) => {
|
||||
setActionLoadingId(planId);
|
||||
|
||||
try {
|
||||
await fertilizationPlanService.deletePlan(planId);
|
||||
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||
if (selectedPlan?.id === planId) {
|
||||
setSelectedPlan(null);
|
||||
setDetailsOpen(false);
|
||||
}
|
||||
} catch (deleteError) {
|
||||
setError(
|
||||
deleteError instanceof Error
|
||||
? deleteError.message
|
||||
: "حذف برنامه کوددهی انجام نشد.",
|
||||
);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowDetails = (plan: FertilizationPlanRow) => {
|
||||
const handleShowDetails = async (plan: FertilizationPlanRow) => {
|
||||
setSelectedPlan(plan);
|
||||
setSelectedRelatedPlanId(relatedIrrigationPlans[0]?.id ?? null);
|
||||
setDetailsOpen(true);
|
||||
};
|
||||
setDetailsLoading(true);
|
||||
|
||||
const handleConfirmRelatedPlan = (relatedPlan: RelatedPlanItem) => {
|
||||
try {
|
||||
const detail = await fertilizationPlanService.getPlanDetail(plan.id);
|
||||
setSelectedPlan(mapFertilizationPlanRow(detail, detail));
|
||||
await loadRelatedPlans();
|
||||
} catch (detailError) {
|
||||
setError(
|
||||
detailError instanceof Error
|
||||
? detailError.message
|
||||
: "دریافت جزئیات برنامه کوددهی انجام نشد.",
|
||||
);
|
||||
setDetailsOpen(false);
|
||||
if (!selectedPlan) return;
|
||||
|
||||
const query = new URLSearchParams({
|
||||
from: "fertilization-plan",
|
||||
planType: "fertilization",
|
||||
planId: String(selectedPlan.id),
|
||||
planName: selectedPlan.planName,
|
||||
relatedType: "irrigation",
|
||||
relatedPlanId: String(relatedPlan.id),
|
||||
relatedPlanName: relatedPlan.title,
|
||||
});
|
||||
|
||||
router.push(`/yield-harvest?${query.toString()}`);
|
||||
} finally {
|
||||
setDetailsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -182,6 +238,9 @@ const FertilizationPlanPage = () => {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack spacing={4}>
|
||||
{loading ? <LinearProgress sx={{ borderRadius: 999 }} /> : null}
|
||||
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
spacing={3}
|
||||
@@ -191,7 +250,7 @@ const FertilizationPlanPage = () => {
|
||||
<Box>
|
||||
<Typography variant="h5">لیست برنامههای کوددهی</Typography>
|
||||
<Typography color="text.secondary">
|
||||
نمایش همه برنامهها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول
|
||||
لیست برنامههای کوددهی ثبتشده برای مزرعه فعال
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -216,9 +275,9 @@ const FertilizationPlanPage = () => {
|
||||
<TableRow>
|
||||
<TableCell>نام برنامه</TableCell>
|
||||
<TableCell>محصول نهایی</TableCell>
|
||||
<TableCell>زمان برداشت</TableCell>
|
||||
<TableCell>تن خروجی</TableCell>
|
||||
<TableCell>نوع کود</TableCell>
|
||||
<TableCell>تاریخ ایجاد</TableCell>
|
||||
<TableCell>مرحله رشد</TableCell>
|
||||
<TableCell>نوع کود / منبع</TableCell>
|
||||
<TableCell>وضعیت</TableCell>
|
||||
<TableCell align="center">عملیات</TableCell>
|
||||
</TableRow>
|
||||
@@ -229,7 +288,7 @@ const FertilizationPlanPage = () => {
|
||||
<TableCell>{plan.planName}</TableCell>
|
||||
<TableCell>{plan.finalProduct}</TableCell>
|
||||
<TableCell>{plan.harvestTime}</TableCell>
|
||||
<TableCell>{plan.outputTon} تن</TableCell>
|
||||
<TableCell>{plan.growthStage}</TableCell>
|
||||
<TableCell>{plan.fertilizerType}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
@@ -246,7 +305,7 @@ const FertilizationPlanPage = () => {
|
||||
<IconButton
|
||||
color="success"
|
||||
onClick={() => handleActivate(plan.id)}
|
||||
disabled={plan.status === "active"}
|
||||
disabled={plan.status === "active" || actionLoadingId === plan.id}
|
||||
>
|
||||
<i className="tabler-player-play text-[20px]" />
|
||||
</IconButton>
|
||||
@@ -266,6 +325,7 @@ const FertilizationPlanPage = () => {
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
disabled={actionLoadingId === plan.id}
|
||||
>
|
||||
<i className="tabler-trash text-[20px]" />
|
||||
</IconButton>
|
||||
@@ -301,25 +361,9 @@ const FertilizationPlanPage = () => {
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Chip label={`محصول نهایی: ${selectedPlan.finalProduct}`} variant="tonal" />
|
||||
<Chip label={`زمان برداشت: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||
<Chip label={`تن خروجی: ${selectedPlan.outputTon} تن`} variant="tonal" />
|
||||
<Chip label={`تاریخ ایجاد: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||
<Chip label={`مرحله رشد: ${selectedPlan.growthStage}`} variant="tonal" />
|
||||
<Chip label={`نوع کود: ${selectedPlan.fertilizerType}`} variant="tonal" />
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<i className="tabler-list-details" />}
|
||||
onClick={() => handleShowDetails(selectedPlan)}
|
||||
>
|
||||
مشاهده و انتخاب برنامه آبیاری
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<i className="tabler-check" />}
|
||||
onClick={() => handleActivate(selectedPlan.id)}
|
||||
>
|
||||
فعال کردن این برنامه
|
||||
</Button>
|
||||
</Stack>
|
||||
) : (
|
||||
<Alert severity="warning">
|
||||
@@ -332,6 +376,8 @@ const FertilizationPlanPage = () => {
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
{detailsLoading ? <LinearProgress sx={{ borderRadius: 999, mt: 2 }} /> : null}
|
||||
|
||||
{selectedPlan ? (
|
||||
<RelatedPlanSelector
|
||||
open={detailsOpen}
|
||||
@@ -340,18 +386,18 @@ const FertilizationPlanPage = () => {
|
||||
description="برای تکمیل اجرای این برنامه کوددهی، یکی از برنامههای آبیاری مرتبط را انتخاب کنید."
|
||||
currentPlanName={selectedPlan.planName}
|
||||
currentPlanStatus={selectedPlan.status === "active" ? "برنامه فعال" : "آماده برای فعالسازی"}
|
||||
currentPlanOutput={`خروجی هدف: ${selectedPlan.outputTon} تن محصول نهایی`}
|
||||
currentPlanOutput={`منبع: ${selectedPlan.sourceLabel}`}
|
||||
summaryItems={[
|
||||
{ label: "محصول نهایی", value: selectedPlan.finalProduct },
|
||||
{ label: "زمان برداشت", value: selectedPlan.harvestTime },
|
||||
{ label: "نوع کود", value: selectedPlan.fertilizerType },
|
||||
{ label: "تاریخ ایجاد", value: selectedPlan.harvestTime },
|
||||
{ label: "جزئیات", value: selectedPlan.fertilizerType },
|
||||
]}
|
||||
relatedTitle="برنامههای آبیاری پیشنهادی"
|
||||
relatedDescription="از بین برنامههای زیر، مناسبترین برنامه آبیاری را برای این برنامه کوددهی انتخاب کنید."
|
||||
relatedPlans={relatedIrrigationPlans}
|
||||
relatedPlans={relatedPlans}
|
||||
selectedRelatedPlanId={selectedRelatedPlanId}
|
||||
onSelectRelatedPlan={setSelectedRelatedPlanId}
|
||||
onConfirm={handleConfirmRelatedPlan}
|
||||
onSelectRelatedPlan={(planId) => setSelectedRelatedPlanId(planId)}
|
||||
onConfirm={() => setDetailsOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import Alert from "@mui/material/Alert";
|
||||
@@ -11,6 +11,7 @@ import CardContent from "@mui/material/CardContent";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
@@ -25,99 +26,77 @@ import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPla
|
||||
import RelatedPlanSelector, {
|
||||
type RelatedPlanItem,
|
||||
} from "@views/dashboards/farm/planSelector/RelatedPlanSelector";
|
||||
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||
import {
|
||||
irrigationPlanService,
|
||||
type IrrigationPlanDetail,
|
||||
type IrrigationPlanListItem,
|
||||
} from "@/libs/api/services/irrigationPlanService";
|
||||
import { fertilizationPlanService } from "@/libs/api/services/fertilizationPlanService";
|
||||
|
||||
import CustomTextField from "@core/components/mui/TextField";
|
||||
|
||||
type IrrigationPlanRow = {
|
||||
id: number;
|
||||
id: string;
|
||||
planName: string;
|
||||
finalProduct: string;
|
||||
harvestTime: string;
|
||||
outputTon: number;
|
||||
irrigationMethod: string;
|
||||
status: "active" | "draft";
|
||||
sourceLabel: string;
|
||||
growthStage: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const mockIrrigationPlans: IrrigationPlanRow[] = [
|
||||
{
|
||||
id: 1,
|
||||
planName: "برنامه آبیاری گوجه فرنگی گلخانهای",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۱۰",
|
||||
outputTon: 48,
|
||||
irrigationMethod: "قطرهای",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
planName: "برنامه آبیاری ذرت علوفهای",
|
||||
finalProduct: "ذرت علوفهای درجه یک",
|
||||
harvestTime: "۱۴۰۴/۰۵/۲۲",
|
||||
outputTon: 36,
|
||||
irrigationMethod: "بارانی",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
planName: "برنامه آبیاری خیار فضای باز",
|
||||
finalProduct: "خیار صادراتی",
|
||||
harvestTime: "۱۴۰۴/۰۴/۱۸",
|
||||
outputTon: 28,
|
||||
irrigationMethod: "تیپ",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
planName: "برنامه آبیاری هندوانه",
|
||||
finalProduct: "هندوانه شیرین بازارپسند",
|
||||
harvestTime: "۱۴۰۴/۰۵/۳۰",
|
||||
outputTon: 55,
|
||||
irrigationMethod: "قطرهای",
|
||||
status: "draft",
|
||||
},
|
||||
];
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const relatedFertilizationPlans: RelatedPlanItem[] = [
|
||||
{
|
||||
id: 201,
|
||||
title: "کوددهی آغاز رشد رویشی",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۵/۲۰",
|
||||
outputTon: 42,
|
||||
methodLabel: "نوع کود: NPK 20-20-20",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
title: "کوددهی تقویت گلدهی",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۰۵",
|
||||
outputTon: 47,
|
||||
methodLabel: "نوع کود: کلسیم بور",
|
||||
status: "draft",
|
||||
},
|
||||
{
|
||||
id: 203,
|
||||
title: "کوددهی پتاس بالا برای رنگگیری",
|
||||
finalProduct: "گوجه فرنگی ممتاز",
|
||||
harvestTime: "۱۴۰۴/۰۶/۱۲",
|
||||
outputTon: 51,
|
||||
methodLabel: "نوع کود: سولفات پتاسیم",
|
||||
status: "draft",
|
||||
},
|
||||
];
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return "—";
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const mapIrrigationPlanRow = (
|
||||
plan: IrrigationPlanListItem,
|
||||
detail?: IrrigationPlanDetail | null,
|
||||
): IrrigationPlanRow => ({
|
||||
id: plan.plan_uuid,
|
||||
planName: plan.title,
|
||||
finalProduct: plan.plant_name || plan.crop_id || "—",
|
||||
harvestTime: formatDate(plan.created_at),
|
||||
outputTon: 0,
|
||||
irrigationMethod:
|
||||
detail?.plan_payload?.plan?.durationMinutes !== undefined
|
||||
? `${detail.plan_payload.plan.durationMinutes} دقیقه`
|
||||
: plan.source_label || "—",
|
||||
status: plan.is_active ? "active" : "draft",
|
||||
sourceLabel: plan.source_label || "—",
|
||||
growthStage: plan.growth_stage || "—",
|
||||
createdAt: plan.created_at,
|
||||
});
|
||||
|
||||
const IrrigationPlanPage = () => {
|
||||
const router = useRouter();
|
||||
const [plans, setPlans] = useState(mockIrrigationPlans);
|
||||
const { getFarmUuid } = useFarmHub();
|
||||
const [plans, setPlans] = useState<IrrigationPlanRow[]>([]);
|
||||
const [minOutputTon, setMinOutputTon] = useState("0");
|
||||
const [selectedPlan, setSelectedPlan] = useState<IrrigationPlanRow | null>(
|
||||
mockIrrigationPlans[0],
|
||||
);
|
||||
const [selectedPlan, setSelectedPlan] = useState<IrrigationPlanRow | null>(null);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | null>(
|
||||
relatedFertilizationPlans[0]?.id ?? null,
|
||||
);
|
||||
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | string | null>(null);
|
||||
const [relatedPlans, setRelatedPlans] = useState<RelatedPlanItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailsLoading, setDetailsLoading] = useState(false);
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
const minTon = Number(minOutputTon);
|
||||
@@ -127,34 +106,134 @@ const IrrigationPlanPage = () => {
|
||||
.sort((firstPlan, secondPlan) => secondPlan.outputTon - firstPlan.outputTon);
|
||||
}, [minOutputTon, plans]);
|
||||
|
||||
const handleActivate = (planId: number) => {
|
||||
const loadPlans = useCallback(async () => {
|
||||
const farmUuid = getFarmUuid();
|
||||
|
||||
if (!farmUuid) {
|
||||
setError("ابتدا مزرعه فعال را انتخاب کنید.");
|
||||
setPlans([]);
|
||||
setSelectedPlan(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await irrigationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
|
||||
const mappedPlans = response.data.map((plan) => mapIrrigationPlanRow(plan));
|
||||
|
||||
setPlans(mappedPlans);
|
||||
setSelectedPlan((current) =>
|
||||
mappedPlans.find((plan) => plan.id === current?.id) ?? mappedPlans[0] ?? null,
|
||||
);
|
||||
} catch (loadError) {
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "دریافت لیست برنامههای آبیاری انجام نشد.",
|
||||
);
|
||||
setPlans([]);
|
||||
setSelectedPlan(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [getFarmUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
}, [loadPlans]);
|
||||
|
||||
const loadRelatedPlans = useCallback(async () => {
|
||||
const farmUuid = getFarmUuid();
|
||||
|
||||
if (!farmUuid) {
|
||||
setRelatedPlans([]);
|
||||
setSelectedRelatedPlanId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fertilizationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
|
||||
const items: RelatedPlanItem[] = response.data.map((plan) => ({
|
||||
id: plan.plan_uuid,
|
||||
title: plan.title,
|
||||
finalProduct: plan.plant_name || plan.crop_id || "—",
|
||||
harvestTime: formatDate(plan.created_at),
|
||||
outputTon: 0,
|
||||
methodLabel: `منبع: ${plan.source_label || "—"}`,
|
||||
status: plan.is_active ? "active" : "draft",
|
||||
}));
|
||||
|
||||
setRelatedPlans(items);
|
||||
setSelectedRelatedPlanId(items[0]?.id ?? null);
|
||||
}, [getFarmUuid]);
|
||||
|
||||
const handleActivate = async (planId: string) => {
|
||||
setActionLoadingId(planId);
|
||||
|
||||
try {
|
||||
await irrigationPlanService.updatePlanStatus(planId, true);
|
||||
setPlans((currentPlans) =>
|
||||
currentPlans.map((plan) => ({
|
||||
...plan,
|
||||
status: plan.id === planId ? "active" : "draft",
|
||||
})),
|
||||
);
|
||||
|
||||
const activePlan = plans.find((plan) => plan.id === planId);
|
||||
|
||||
if (activePlan) {
|
||||
setSelectedPlan({ ...activePlan, status: "active" });
|
||||
setSelectedPlan((currentPlan) =>
|
||||
currentPlan && currentPlan.id === planId
|
||||
? { ...currentPlan, status: "active" }
|
||||
: currentPlan,
|
||||
);
|
||||
} catch (updateError) {
|
||||
setError(
|
||||
updateError instanceof Error
|
||||
? updateError.message
|
||||
: "تغییر وضعیت برنامه آبیاری انجام نشد.",
|
||||
);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (planId: number) => {
|
||||
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||
const handleDelete = async (planId: string) => {
|
||||
setActionLoadingId(planId);
|
||||
|
||||
try {
|
||||
await irrigationPlanService.deletePlan(planId);
|
||||
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||
if (selectedPlan?.id === planId) {
|
||||
setSelectedPlan(null);
|
||||
setDetailsOpen(false);
|
||||
}
|
||||
} catch (deleteError) {
|
||||
setError(
|
||||
deleteError instanceof Error
|
||||
? deleteError.message
|
||||
: "حذف برنامه آبیاری انجام نشد.",
|
||||
);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowDetails = (plan: IrrigationPlanRow) => {
|
||||
const handleShowDetails = async (plan: IrrigationPlanRow) => {
|
||||
setSelectedPlan(plan);
|
||||
setSelectedRelatedPlanId(relatedFertilizationPlans[0]?.id ?? null);
|
||||
setDetailsOpen(true);
|
||||
setDetailsLoading(true);
|
||||
|
||||
try {
|
||||
const detail = await irrigationPlanService.getPlanDetail(plan.id);
|
||||
setSelectedPlan(mapIrrigationPlanRow(detail, detail));
|
||||
await loadRelatedPlans();
|
||||
} catch (detailError) {
|
||||
setError(
|
||||
detailError instanceof Error
|
||||
? detailError.message
|
||||
: "دریافت جزئیات برنامه آبیاری انجام نشد.",
|
||||
);
|
||||
} finally {
|
||||
setDetailsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmRelatedPlan = (relatedPlan: RelatedPlanItem) => {
|
||||
@@ -185,6 +264,9 @@ const IrrigationPlanPage = () => {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack spacing={4}>
|
||||
{loading ? <LinearProgress sx={{ borderRadius: 999 }} /> : null}
|
||||
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
spacing={3}
|
||||
@@ -194,7 +276,7 @@ const IrrigationPlanPage = () => {
|
||||
<Box>
|
||||
<Typography variant="h5">لیست برنامههای آبیاری</Typography>
|
||||
<Typography color="text.secondary">
|
||||
نمایش همه برنامهها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول
|
||||
لیست برنامههای آبیاری ثبتشده برای مزرعه فعال
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -219,9 +301,9 @@ const IrrigationPlanPage = () => {
|
||||
<TableRow>
|
||||
<TableCell>نام برنامه</TableCell>
|
||||
<TableCell>محصول نهایی</TableCell>
|
||||
<TableCell>زمان برداشت</TableCell>
|
||||
<TableCell>تن خروجی</TableCell>
|
||||
<TableCell>روش آبیاری</TableCell>
|
||||
<TableCell>تاریخ ایجاد</TableCell>
|
||||
<TableCell>مرحله رشد</TableCell>
|
||||
<TableCell>منبع / جزئیات</TableCell>
|
||||
<TableCell>وضعیت</TableCell>
|
||||
<TableCell align="center">عملیات</TableCell>
|
||||
</TableRow>
|
||||
@@ -232,7 +314,7 @@ const IrrigationPlanPage = () => {
|
||||
<TableCell>{plan.planName}</TableCell>
|
||||
<TableCell>{plan.finalProduct}</TableCell>
|
||||
<TableCell>{plan.harvestTime}</TableCell>
|
||||
<TableCell>{plan.outputTon} تن</TableCell>
|
||||
<TableCell>{plan.growthStage}</TableCell>
|
||||
<TableCell>{plan.irrigationMethod}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
@@ -249,7 +331,7 @@ const IrrigationPlanPage = () => {
|
||||
<IconButton
|
||||
color="success"
|
||||
onClick={() => handleActivate(plan.id)}
|
||||
disabled={plan.status === "active"}
|
||||
disabled={plan.status === "active" || actionLoadingId === plan.id}
|
||||
>
|
||||
<i className="tabler-player-play text-[20px]" />
|
||||
</IconButton>
|
||||
@@ -269,6 +351,7 @@ const IrrigationPlanPage = () => {
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
disabled={actionLoadingId === plan.id}
|
||||
>
|
||||
<i className="tabler-trash text-[20px]" />
|
||||
</IconButton>
|
||||
@@ -304,9 +387,9 @@ const IrrigationPlanPage = () => {
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Chip label={`محصول نهایی: ${selectedPlan.finalProduct}`} variant="tonal" />
|
||||
<Chip label={`زمان برداشت: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||
<Chip label={`تن خروجی: ${selectedPlan.outputTon} تن`} variant="tonal" />
|
||||
<Chip label={`روش آبیاری: ${selectedPlan.irrigationMethod}`} variant="tonal" />
|
||||
<Chip label={`تاریخ ایجاد: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||
<Chip label={`مرحله رشد: ${selectedPlan.growthStage}`} variant="tonal" />
|
||||
<Chip label={`جزئیات: ${selectedPlan.irrigationMethod}`} variant="tonal" />
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@@ -320,6 +403,7 @@ const IrrigationPlanPage = () => {
|
||||
color="success"
|
||||
startIcon={<i className="tabler-check" />}
|
||||
onClick={() => handleActivate(selectedPlan.id)}
|
||||
disabled={actionLoadingId === selectedPlan.id}
|
||||
>
|
||||
فعال کردن این برنامه
|
||||
</Button>
|
||||
@@ -343,17 +427,17 @@ const IrrigationPlanPage = () => {
|
||||
description="برای تکمیل اجرای این برنامه آبیاری، یکی از برنامههای کوددهی مرتبط را انتخاب کنید."
|
||||
currentPlanName={selectedPlan.planName}
|
||||
currentPlanStatus={selectedPlan.status === "active" ? "برنامه فعال" : "آماده برای فعالسازی"}
|
||||
currentPlanOutput={`خروجی هدف: ${selectedPlan.outputTon} تن محصول نهایی`}
|
||||
currentPlanOutput={`منبع: ${selectedPlan.sourceLabel}`}
|
||||
summaryItems={[
|
||||
{ label: "محصول نهایی", value: selectedPlan.finalProduct },
|
||||
{ label: "زمان برداشت", value: selectedPlan.harvestTime },
|
||||
{ label: "روش آبیاری", value: selectedPlan.irrigationMethod },
|
||||
{ label: "تاریخ ایجاد", value: selectedPlan.harvestTime },
|
||||
{ label: "جزئیات", value: selectedPlan.irrigationMethod },
|
||||
]}
|
||||
relatedTitle="برنامههای کوددهی پیشنهادی"
|
||||
relatedDescription="از بین برنامههای زیر، مناسبترین برنامه کوددهی را برای این برنامه آبیاری انتخاب کنید."
|
||||
relatedPlans={relatedFertilizationPlans}
|
||||
relatedPlans={relatedPlans}
|
||||
selectedRelatedPlanId={selectedRelatedPlanId}
|
||||
onSelectRelatedPlan={setSelectedRelatedPlanId}
|
||||
onSelectRelatedPlan={(planId) => setSelectedRelatedPlanId(planId)}
|
||||
onConfirm={handleConfirmRelatedPlan}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -20,23 +20,50 @@ const YieldHarvestPage = () => {
|
||||
const selectionSummary = useMemo(() => {
|
||||
const from = searchParams.get("from");
|
||||
const planType = searchParams.get("planType");
|
||||
const planId = searchParams.get("planId");
|
||||
const planName = searchParams.get("planName");
|
||||
const relatedType = searchParams.get("relatedType");
|
||||
const relatedPlanId = searchParams.get("relatedPlanId");
|
||||
const relatedPlanName = searchParams.get("relatedPlanName");
|
||||
|
||||
if (!from || !planType || !planName || !relatedType || !relatedPlanName) {
|
||||
if (!from || !planType || !planId || !planName || !relatedType || !relatedPlanId || !relatedPlanName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
planType,
|
||||
planId,
|
||||
planName,
|
||||
relatedType,
|
||||
relatedPlanId,
|
||||
relatedPlanName,
|
||||
};
|
||||
}, [searchParams]);
|
||||
|
||||
const planContext = useMemo(() => {
|
||||
if (!selectionSummary) return undefined;
|
||||
|
||||
const context: {
|
||||
irrigationPlanId?: string;
|
||||
fertilizationPlanId?: string;
|
||||
} = {};
|
||||
|
||||
if (selectionSummary.planType === "irrigation") {
|
||||
context.irrigationPlanId = selectionSummary.planId;
|
||||
} else {
|
||||
context.fertilizationPlanId = selectionSummary.planId;
|
||||
}
|
||||
|
||||
if (selectionSummary.relatedType === "irrigation") {
|
||||
context.irrigationPlanId = selectionSummary.relatedPlanId;
|
||||
} else {
|
||||
context.fertilizationPlanId = selectionSummary.relatedPlanId;
|
||||
}
|
||||
|
||||
return context;
|
||||
}, [selectionSummary]);
|
||||
|
||||
const handleBack = () => {
|
||||
const from = selectionSummary?.from;
|
||||
|
||||
@@ -97,7 +124,7 @@ const YieldHarvestPage = () => {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<PlantProductionPage />
|
||||
<PlantProductionPage planContext={planContext} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { apiClient } from "../client";
|
||||
|
||||
const PREFIX = "/api/fertilization";
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PlanPagination {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
total_items: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
}
|
||||
|
||||
interface PaginatedApiResponse<T> extends ApiResponse<T> {
|
||||
pagination: PlanPagination;
|
||||
}
|
||||
|
||||
export interface FertilizationPlanListItem {
|
||||
plan_uuid: string;
|
||||
source: string;
|
||||
source_label: string;
|
||||
title: string;
|
||||
crop_id: string | null;
|
||||
plant_name: string | null;
|
||||
growth_stage: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface FertilizationPlanDetail extends FertilizationPlanListItem {
|
||||
updated_at: string;
|
||||
plan_payload: {
|
||||
title?: string;
|
||||
items?: Array<{
|
||||
name?: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||
const response = await promise;
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const fertilizationPlanService = {
|
||||
async getPlans(farmUuid: string, page = 1, pageSize = 10) {
|
||||
const response = await apiClient.get<
|
||||
PaginatedApiResponse<FertilizationPlanListItem[]>
|
||||
>(
|
||||
`${PREFIX}/plans/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
|
||||
getPlanDetail(planUuid: string): Promise<FertilizationPlanDetail> {
|
||||
return unwrap(
|
||||
apiClient.get<ApiResponse<FertilizationPlanDetail>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/`,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
deletePlan(planUuid: string): Promise<{ plan_uuid: string; is_deleted: boolean }> {
|
||||
return unwrap(
|
||||
apiClient.delete<ApiResponse<{ plan_uuid: string; is_deleted: boolean }>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/`,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
updatePlanStatus(
|
||||
planUuid: string,
|
||||
isActive: boolean,
|
||||
): Promise<{ plan_uuid: string; is_active: boolean }> {
|
||||
return unwrap(
|
||||
apiClient.patch<ApiResponse<{ plan_uuid: string; is_active: boolean }>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/status/`,
|
||||
{ is_active: isActive },
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { apiClient } from "../client";
|
||||
|
||||
const PREFIX = "/api/irrigation";
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PlanPagination {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
total_items: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
}
|
||||
|
||||
interface PaginatedApiResponse<T> extends ApiResponse<T> {
|
||||
pagination: PlanPagination;
|
||||
}
|
||||
|
||||
export interface IrrigationPlanListItem {
|
||||
plan_uuid: string;
|
||||
source: string;
|
||||
source_label: string;
|
||||
title: string;
|
||||
crop_id: string | null;
|
||||
plant_name: string | null;
|
||||
growth_stage: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface IrrigationPlanDetail extends IrrigationPlanListItem {
|
||||
updated_at: string;
|
||||
plan_payload: {
|
||||
plan?: {
|
||||
durationMinutes?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||
const response = await promise;
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const irrigationPlanService = {
|
||||
async getPlans(farmUuid: string, page = 1, pageSize = 10) {
|
||||
const response = await apiClient.get<
|
||||
PaginatedApiResponse<IrrigationPlanListItem[]>
|
||||
>(
|
||||
`${PREFIX}/plans/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
|
||||
getPlanDetail(planUuid: string): Promise<IrrigationPlanDetail> {
|
||||
return unwrap(
|
||||
apiClient.get<ApiResponse<IrrigationPlanDetail>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/`,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
deletePlan(planUuid: string): Promise<{ plan_uuid: string; is_deleted: boolean }> {
|
||||
return unwrap(
|
||||
apiClient.delete<ApiResponse<{ plan_uuid: string; is_deleted: boolean }>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/`,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
updatePlanStatus(
|
||||
planUuid: string,
|
||||
isActive: boolean,
|
||||
): Promise<{ plan_uuid: string; is_active: boolean }> {
|
||||
return unwrap(
|
||||
apiClient.patch<ApiResponse<{ plan_uuid: string; is_active: boolean }>>(
|
||||
`${PREFIX}/plans/${encodeURIComponent(planUuid)}/status/`,
|
||||
{ is_active: isActive },
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -113,6 +113,11 @@ interface ApiResponse<T> {
|
||||
result?: T
|
||||
}
|
||||
|
||||
export interface YieldHarvestPlanContext {
|
||||
irrigationPlanId?: string | number | null
|
||||
fertilizationPlanId?: string | number | null
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | T): T {
|
||||
if (res && typeof res === 'object') {
|
||||
if ('data' in res) return (res as ApiResponse<T>).data
|
||||
@@ -122,6 +127,16 @@ function extract<T>(res: ApiResponse<T> | T): T {
|
||||
return res as T
|
||||
}
|
||||
|
||||
function normalizePlanId(value: string | number | null | undefined): string | number | undefined {
|
||||
if (value === null || value === undefined || value === '') return undefined
|
||||
|
||||
if (typeof value === 'number') return value
|
||||
|
||||
const parsed = Number(value)
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : value
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): GenericRecord | undefined {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as GenericRecord)
|
||||
@@ -142,8 +157,19 @@ function toNumber(value: unknown): number | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function buildFarmPayload(farmUuid: string) {
|
||||
return { farm_uuid: farmUuid }
|
||||
function buildFarmPayload(
|
||||
farmUuid: string,
|
||||
planContext?: YieldHarvestPlanContext,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
farm_uuid: farmUuid,
|
||||
...(normalizePlanId(planContext?.irrigationPlanId) !== undefined
|
||||
? { irrigation_plan_id: normalizePlanId(planContext?.irrigationPlanId) }
|
||||
: {}),
|
||||
...(normalizePlanId(planContext?.fertilizationPlanId) !== undefined
|
||||
? { fertilization_plan_id: normalizePlanId(planContext?.fertilizationPlanId) }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
function buildSummaryQuery(farmUuid: string, options?: SummaryOptions) {
|
||||
@@ -864,10 +890,13 @@ function enrichSummaryData(
|
||||
}
|
||||
|
||||
export const yieldHarvestService = {
|
||||
async getCurrentFarmChart(farmUuid: string): Promise<CurrentFarmChartResponse> {
|
||||
async getCurrentFarmChart(
|
||||
farmUuid: string,
|
||||
planContext?: YieldHarvestPlanContext,
|
||||
): Promise<CurrentFarmChartResponse> {
|
||||
const response = await apiClient.post<
|
||||
ApiResponse<CurrentFarmChartResponse> | CurrentFarmChartResponse
|
||||
>(`${PREFIX}/current-farm-chart/`, buildFarmPayload(farmUuid))
|
||||
>(`${PREFIX}/current-farm-chart/`, buildFarmPayload(farmUuid, planContext))
|
||||
|
||||
return extract(response)
|
||||
},
|
||||
@@ -905,18 +934,22 @@ export const yieldHarvestService = {
|
||||
|
||||
async getHarvestPrediction(
|
||||
farmUuid: string,
|
||||
planContext?: YieldHarvestPlanContext,
|
||||
): Promise<HarvestPredictionResponse> {
|
||||
const response = await apiClient.post<
|
||||
ApiResponse<HarvestPredictionResponse> | HarvestPredictionResponse
|
||||
>(`${PREFIX}/harvest-prediction/`, buildFarmPayload(farmUuid))
|
||||
>(`${PREFIX}/harvest-prediction/`, buildFarmPayload(farmUuid, planContext))
|
||||
|
||||
return extract(response)
|
||||
},
|
||||
|
||||
async getYieldPrediction(farmUuid: string): Promise<YieldPredictionResponse> {
|
||||
async getYieldPrediction(
|
||||
farmUuid: string,
|
||||
planContext?: YieldHarvestPlanContext,
|
||||
): Promise<YieldPredictionResponse> {
|
||||
const response = await apiClient.post<
|
||||
ApiResponse<YieldPredictionResponse> | YieldPredictionResponse
|
||||
>(`${PREFIX}/yield-prediction/`, buildFarmPayload(farmUuid))
|
||||
>(`${PREFIX}/yield-prediction/`, buildFarmPayload(farmUuid, planContext))
|
||||
|
||||
return extract(response)
|
||||
},
|
||||
@@ -935,14 +968,15 @@ export const yieldHarvestService = {
|
||||
|
||||
async getDashboardData(
|
||||
farmUuid: string,
|
||||
planContext?: YieldHarvestPlanContext,
|
||||
options?: SummaryOptions,
|
||||
): Promise<YieldHarvestDashboardData> {
|
||||
const [summaryResult, harvestResult, yieldResult, currentChartResult] =
|
||||
await Promise.allSettled([
|
||||
this.getSummary(farmUuid, options),
|
||||
this.getHarvestPrediction(farmUuid),
|
||||
this.getYieldPrediction(farmUuid),
|
||||
this.getCurrentFarmChart(farmUuid),
|
||||
this.getHarvestPrediction(farmUuid, planContext),
|
||||
this.getYieldPrediction(farmUuid, planContext),
|
||||
this.getCurrentFarmChart(farmUuid, planContext),
|
||||
])
|
||||
|
||||
const summary =
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFarmHub } from "@/hooks/useFarmHub";
|
||||
import { yieldHarvestService } from "@/libs/api/services/yieldHarvestService";
|
||||
import type {
|
||||
CurrentFarmChartResponse,
|
||||
YieldHarvestPlanContext,
|
||||
YieldHarvestSummary,
|
||||
} from "@/libs/api/services/yieldHarvestService";
|
||||
|
||||
@@ -17,7 +18,11 @@ import YieldSeasonHighlightsCard from "@views/dashboards/farm/YieldSeasonHighlig
|
||||
import PlantSimulator from "@views/dashboards/farm/plantSimulator/PlantSimulator";
|
||||
import { mockYieldHarvestSummary } from "@views/dashboards/farm/yieldHarvestMockData";
|
||||
|
||||
const PlantProductionPage = () => {
|
||||
type PlantProductionPageProps = {
|
||||
planContext?: YieldHarvestPlanContext;
|
||||
};
|
||||
|
||||
const PlantProductionPage = ({ planContext }: PlantProductionPageProps) => {
|
||||
const { farmHub } = useFarmHub();
|
||||
const farmUuid = farmHub?.farm_uuid;
|
||||
const [summary, setSummary] =
|
||||
@@ -36,7 +41,7 @@ const PlantProductionPage = () => {
|
||||
|
||||
setLoading(true);
|
||||
return yieldHarvestService
|
||||
.getDashboardData(farmUuid)
|
||||
.getDashboardData(farmUuid, planContext)
|
||||
.then((data) => {
|
||||
setSummary(data.summary);
|
||||
setCurrentFarmChart(data.currentFarmChart ?? null);
|
||||
@@ -46,7 +51,7 @@ const PlantProductionPage = () => {
|
||||
setCurrentFarmChart(null);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [farmUuid]);
|
||||
}, [farmUuid, planContext]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
|
||||
+391
-1020
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import Typography from "@mui/material/Typography";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
export type RelatedPlanItem = {
|
||||
id: number;
|
||||
id: number | string;
|
||||
title: string;
|
||||
finalProduct: string;
|
||||
harvestTime: string;
|
||||
@@ -46,8 +46,8 @@ type RelatedPlanSelectorProps = {
|
||||
relatedTitle: string;
|
||||
relatedDescription: string;
|
||||
relatedPlans: RelatedPlanItem[];
|
||||
selectedRelatedPlanId: number | null;
|
||||
onSelectRelatedPlan: (planId: number) => void;
|
||||
selectedRelatedPlanId: number | string | null;
|
||||
onSelectRelatedPlan: (planId: number | string) => void;
|
||||
onConfirm: (selectedPlan: RelatedPlanItem) => void;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user