This commit is contained in:
2026-05-02 16:31:23 +03:30
parent f8d1f84ed6
commit baf3f01dbc
9 changed files with 1386 additions and 1626 deletions
@@ -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) => {
setPlans((currentPlans) =>
currentPlans.map((plan) => ({
...plan,
status: plan.id === planId ? "active" : "draft",
})),
);
const loadPlans = useCallback(async () => {
const farmUuid = getFarmUuid();
const activePlan = plans.find((plan) => plan.id === planId);
if (activePlan) {
setSelectedPlan({ ...activePlan, status: "active" });
}
};
const handleDelete = (planId: number) => {
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
if (selectedPlan?.id === planId) {
if (!farmUuid) {
setError("ابتدا مزرعه فعال را انتخاب کنید.");
setPlans([]);
setSelectedPlan(null);
setDetailsOpen(false);
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",
})),
);
setSelectedPlan((currentPlan) =>
currentPlan && currentPlan.id === planId
? { ...currentPlan, status: "active" }
: currentPlan,
);
} catch (updateError) {
setError(
updateError instanceof Error
? updateError.message
: "تغییر وضعیت برنامه کوددهی انجام نشد.",
);
} finally {
setActionLoadingId(null);
}
};
const handleShowDetails = (plan: FertilizationPlanRow) => {
setSelectedPlan(plan);
setSelectedRelatedPlanId(relatedIrrigationPlans[0]?.id ?? null);
setDetailsOpen(true);
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);
}
} catch (deleteError) {
setError(
deleteError instanceof Error
? deleteError.message
: "حذف برنامه کوددهی انجام نشد.",
);
} finally {
setActionLoadingId(null);
}
};
const handleConfirmRelatedPlan = (relatedPlan: RelatedPlanItem) => {
setDetailsOpen(false);
if (!selectedPlan) return;
const handleShowDetails = async (plan: FertilizationPlanRow) => {
setSelectedPlan(plan);
setDetailsOpen(true);
setDetailsLoading(true);
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()}`);
try {
const detail = await fertilizationPlanService.getPlanDetail(plan.id);
setSelectedPlan(mapFertilizationPlanRow(detail, detail));
await loadRelatedPlans();
} catch (detailError) {
setError(
detailError instanceof Error
? detailError.message
: "دریافت جزئیات برنامه کوددهی انجام نشد.",
);
setDetailsOpen(false);
} 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}
</>