diff --git a/src/app/(dashboard)/(private)/fertilization-plan/page.tsx b/src/app/(dashboard)/(private)/fertilization-plan/page.tsx index cbf2374..9d2a346 100644 --- a/src/app/(dashboard)/(private)/fertilization-plan/page.tsx +++ b/src/app/(dashboard)/(private)/fertilization-plan/page.tsx @@ -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([]); const [minOutputTon, setMinOutputTon] = useState("0"); - const [selectedPlan, setSelectedPlan] = useState( - mockFertilizationPlans[0], - ); + const [selectedPlan, setSelectedPlan] = useState(null); const [detailsOpen, setDetailsOpen] = useState(false); - const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState( - relatedIrrigationPlans[0]?.id ?? null, + const [loading, setLoading] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [actionLoadingId, setActionLoadingId] = useState(null); + const [error, setError] = useState(null); + const [relatedPlans, setRelatedPlans] = useState([]); + const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState( + 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 = () => { + {loading ? : null} + {error ? {error} : null} + { لیست برنامه‌های کوددهی - نمایش همه برنامه‌ها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول + لیست برنامه‌های کوددهی ثبت‌شده برای مزرعه فعال @@ -216,9 +275,9 @@ const FertilizationPlanPage = () => { نام برنامه محصول نهایی - زمان برداشت - تن خروجی - نوع کود + تاریخ ایجاد + مرحله رشد + نوع کود / منبع وضعیت عملیات @@ -229,7 +288,7 @@ const FertilizationPlanPage = () => { {plan.planName} {plan.finalProduct} {plan.harvestTime} - {plan.outputTon} تن + {plan.growthStage} {plan.fertilizerType} { handleActivate(plan.id)} - disabled={plan.status === "active"} + disabled={plan.status === "active" || actionLoadingId === plan.id} > @@ -266,6 +325,7 @@ const FertilizationPlanPage = () => { handleDelete(plan.id)} + disabled={actionLoadingId === plan.id} > @@ -301,25 +361,9 @@ const FertilizationPlanPage = () => { flexWrap="wrap" > - - + + - - ) : ( @@ -332,6 +376,8 @@ const FertilizationPlanPage = () => { + {detailsLoading ? : null} + {selectedPlan ? ( { 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} diff --git a/src/app/(dashboard)/(private)/irrigation-plan/page.tsx b/src/app/(dashboard)/(private)/irrigation-plan/page.tsx index 904ad16..b0251a0 100644 --- a/src/app/(dashboard)/(private)/irrigation-plan/page.tsx +++ b/src/app/(dashboard)/(private)/irrigation-plan/page.tsx @@ -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([]); const [minOutputTon, setMinOutputTon] = useState("0"); - const [selectedPlan, setSelectedPlan] = useState( - mockIrrigationPlans[0], - ); + const [selectedPlan, setSelectedPlan] = useState(null); const [detailsOpen, setDetailsOpen] = useState(false); - const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState( - relatedFertilizationPlans[0]?.id ?? null, - ); + const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState(null); + const [relatedPlans, setRelatedPlans] = useState([]); + const [loading, setLoading] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [actionLoadingId, setActionLoadingId] = useState(null); + const [error, setError] = useState(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) => { - 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 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", + })), + ); + setSelectedPlan((currentPlan) => + currentPlan && currentPlan.id === planId + ? { ...currentPlan, status: "active" } + : currentPlan, + ); + } catch (updateError) { + setError( + updateError instanceof Error + ? updateError.message + : "تغییر وضعیت برنامه آبیاری انجام نشد.", + ); + } finally { + setActionLoadingId(null); } }; - const handleShowDetails = (plan: IrrigationPlanRow) => { + 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 = 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 = () => { + {loading ? : null} + {error ? {error} : null} + { لیست برنامه‌های آبیاری - نمایش همه برنامه‌ها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول + لیست برنامه‌های آبیاری ثبت‌شده برای مزرعه فعال @@ -219,9 +301,9 @@ const IrrigationPlanPage = () => { نام برنامه محصول نهایی - زمان برداشت - تن خروجی - روش آبیاری + تاریخ ایجاد + مرحله رشد + منبع / جزئیات وضعیت عملیات @@ -232,7 +314,7 @@ const IrrigationPlanPage = () => { {plan.planName} {plan.finalProduct} {plan.harvestTime} - {plan.outputTon} تن + {plan.growthStage} {plan.irrigationMethod} { handleActivate(plan.id)} - disabled={plan.status === "active"} + disabled={plan.status === "active" || actionLoadingId === plan.id} > @@ -269,6 +351,7 @@ const IrrigationPlanPage = () => { handleDelete(plan.id)} + disabled={actionLoadingId === plan.id} > @@ -304,9 +387,9 @@ const IrrigationPlanPage = () => { flexWrap="wrap" > - - - + + + @@ -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} diff --git a/src/app/(dashboard)/(private)/yield-harvest/page.tsx b/src/app/(dashboard)/(private)/yield-harvest/page.tsx index 8d5d7be..7da1383 100644 --- a/src/app/(dashboard)/(private)/yield-harvest/page.tsx +++ b/src/app/(dashboard)/(private)/yield-harvest/page.tsx @@ -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 = () => { ) : null} - + ); }; diff --git a/src/libs/api/services/fertilizationPlanService.ts b/src/libs/api/services/fertilizationPlanService.ts new file mode 100644 index 0000000..82f556e --- /dev/null +++ b/src/libs/api/services/fertilizationPlanService.ts @@ -0,0 +1,97 @@ +import { apiClient } from "../client"; + +const PREFIX = "/api/fertilization"; + +interface ApiResponse { + 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 extends ApiResponse { + 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(promise: Promise>): Promise { + const response = await promise; + + return response.data; +} + +export const fertilizationPlanService = { + async getPlans(farmUuid: string, page = 1, pageSize = 10) { + const response = await apiClient.get< + PaginatedApiResponse + >( + `${PREFIX}/plans/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`, + ); + + return { + data: response.data, + pagination: response.pagination, + }; + }, + + getPlanDetail(planUuid: string): Promise { + return unwrap( + apiClient.get>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/`, + ), + ); + }, + + deletePlan(planUuid: string): Promise<{ plan_uuid: string; is_deleted: boolean }> { + return unwrap( + apiClient.delete>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/`, + ), + ); + }, + + updatePlanStatus( + planUuid: string, + isActive: boolean, + ): Promise<{ plan_uuid: string; is_active: boolean }> { + return unwrap( + apiClient.patch>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/status/`, + { is_active: isActive }, + ), + ); + }, +}; diff --git a/src/libs/api/services/irrigationPlanService.ts b/src/libs/api/services/irrigationPlanService.ts new file mode 100644 index 0000000..2c8151e --- /dev/null +++ b/src/libs/api/services/irrigationPlanService.ts @@ -0,0 +1,96 @@ +import { apiClient } from "../client"; + +const PREFIX = "/api/irrigation"; + +interface ApiResponse { + 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 extends ApiResponse { + 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(promise: Promise>): Promise { + const response = await promise; + + return response.data; +} + +export const irrigationPlanService = { + async getPlans(farmUuid: string, page = 1, pageSize = 10) { + const response = await apiClient.get< + PaginatedApiResponse + >( + `${PREFIX}/plans/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`, + ); + + return { + data: response.data, + pagination: response.pagination, + }; + }, + + getPlanDetail(planUuid: string): Promise { + return unwrap( + apiClient.get>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/`, + ), + ); + }, + + deletePlan(planUuid: string): Promise<{ plan_uuid: string; is_deleted: boolean }> { + return unwrap( + apiClient.delete>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/`, + ), + ); + }, + + updatePlanStatus( + planUuid: string, + isActive: boolean, + ): Promise<{ plan_uuid: string; is_active: boolean }> { + return unwrap( + apiClient.patch>( + `${PREFIX}/plans/${encodeURIComponent(planUuid)}/status/`, + { is_active: isActive }, + ), + ); + }, +}; diff --git a/src/libs/api/services/yieldHarvestService.ts b/src/libs/api/services/yieldHarvestService.ts index 3d04ffe..d3b2197 100644 --- a/src/libs/api/services/yieldHarvestService.ts +++ b/src/libs/api/services/yieldHarvestService.ts @@ -113,6 +113,11 @@ interface ApiResponse { result?: T } +export interface YieldHarvestPlanContext { + irrigationPlanId?: string | number | null + fertilizationPlanId?: string | number | null +} + function extract(res: ApiResponse | T): T { if (res && typeof res === 'object') { if ('data' in res) return (res as ApiResponse).data @@ -122,6 +127,16 @@ function extract(res: ApiResponse | 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 { + 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 { + async getCurrentFarmChart( + farmUuid: string, + planContext?: YieldHarvestPlanContext, + ): Promise { const response = await apiClient.post< ApiResponse | 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 { const response = await apiClient.post< ApiResponse | HarvestPredictionResponse - >(`${PREFIX}/harvest-prediction/`, buildFarmPayload(farmUuid)) + >(`${PREFIX}/harvest-prediction/`, buildFarmPayload(farmUuid, planContext)) return extract(response) }, - async getYieldPrediction(farmUuid: string): Promise { + async getYieldPrediction( + farmUuid: string, + planContext?: YieldHarvestPlanContext, + ): Promise { const response = await apiClient.post< ApiResponse | 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 { 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 = diff --git a/src/views/dashboards/farm/PlantProductionPage.tsx b/src/views/dashboards/farm/PlantProductionPage.tsx index 1986cc2..b9bc13f 100644 --- a/src/views/dashboards/farm/PlantProductionPage.tsx +++ b/src/views/dashboards/farm/PlantProductionPage.tsx @@ -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(); diff --git a/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx b/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx index 0fbc5f5..7adfdd9 100644 --- a/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx +++ b/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx @@ -15,14 +15,15 @@ import Stack from "@mui/material/Stack"; import Step from "@mui/material/Step"; import StepLabel from "@mui/material/StepLabel"; import Stepper from "@mui/material/Stepper"; -import Tab from "@mui/material/Tab"; -import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import { alpha, useTheme } from "@mui/material/styles"; import type { ThemeColor } from "@core/types"; import HorizontalWithAvatar from "@/components/card-statistics/HorizontalWithAvatar"; +import RelatedPlanSelector, { + type RelatedPlanItem, +} from "@/views/dashboards/farm/planSelector/RelatedPlanSelector"; import { useFarmHub } from "@/hooks/useFarmHub"; import { fertilizationPlanParserService, @@ -31,21 +32,11 @@ import { type FertilizationPlanData, type FertilizationPlanParserResult, } from "@/libs/api/services/fertilizationPlanParserService"; -import { - irrigationPlanParserService, - type IrrigationPlanAnswerValue, - type IrrigationPlanData, - type IrrigationPlanParserResult, -} from "@/libs/api/services/irrigationPlanParserService"; +import { irrigationPlanService } from "@/libs/api/services/irrigationPlanService"; import OptionMenu from "@core/components/option-menu"; import CustomAvatar from "@core/components/mui/Avatar"; import CustomTextField from "@core/components/mui/TextField"; -type ParserTabKey = "fertilization" | "irrigation"; -type ParserAnswerValue = - | FertilizationPlanAnswerValue - | IrrigationPlanAnswerValue; - type ParserQuestion = { id: string; field: string; @@ -53,13 +44,9 @@ type ParserQuestion = { rationale: string; }; -type ParserResponse = - | FertilizationPlanParserResult - | IrrigationPlanParserResult; - -type TabState = { +type ParserState = { message: string; - response: ParserResponse | null; + response: FertilizationPlanParserResult | null; answers: Record; activeQuestion: number; requestError: string | null; @@ -73,46 +60,46 @@ type FieldMeta = { value: string | null; }; -type ParserConfig = { - key: ParserTabKey; - label: string; - badge: string; - icon: string; - heroTitle: string; - heroDescription: string; - panelTitle: string; - panelDescription: string; - inputLabel: string; - inputPlaceholder: string; - samplePrompts: string[]; - fieldLabels: Record; - previewOrder: string[]; - itemCountTitle: string; - primaryCardTitle: string; - primaryFallbackTitle: string; - finalCardTitle: string; - finalCardDescription: string; - buildPayload: (payload: { - message?: string; - answers?: Record; - partialPlan?: unknown; - farmUuid?: string; - }) => Record; - submit: (payload: Record) => Promise; - getFieldValue: (plan: unknown, field: string) => ParserAnswerValue; - formatFieldValue: (field: string, value: ParserAnswerValue) => string; - getPrimaryItem: (plan: unknown) => unknown | null; - getPrimaryMeta: (item: unknown) => FieldMeta[]; - getPrimaryHeadline: (item: unknown) => string; - getItemCount: (plan: unknown) => number; -}; - type FertilizationPlanParserPageProps = { - initialTab?: ParserTabKey; - enabledTabs?: ParserTabKey[]; + initialTab?: unknown; + enabledTabs?: unknown; }; -const createInitialTabState = (): TabState => ({ +const FERTILIZATION_FIELD_LABELS: Record = { + crop_name: "محصول", + growth_stage: "مرحله رشد", + objective: "هدف برنامه", + fertilizer_name: "نام کود", + formula: "فرمول کود", + amount: "مقدار مصرف", + application_method: "روش مصرف", + timing: "زمان بندی", + interval_days: "فاصله نوبت ها", + purpose: "هدف هر نوبت", +}; + +const PREVIEW_ORDER = [ + "crop_name", + "growth_stage", + "objective", + "fertilizer_name", + "formula", + "amount", + "application_method", + "timing", + "interval_days", + "purpose", +]; + +const SAMPLE_PROMPTS = [ + "برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.", + "برای ذرت از کود کامل استفاده می کنم اما فاصله بین نوبت ها را دقیق نمی دانم و می خواهم برنامه ام کامل شود.", + "برای گوجه فرنگی در شروع گلدهی یک برنامه کودهی دقیق با کود NPK و مصرف مرحله ای می خواهم.", +]; + +const PAGE_SIZE = 10; + +const createInitialState = (): ParserState => ({ message: "", response: null, answers: {}, @@ -122,37 +109,23 @@ const createInitialTabState = (): TabState => ({ loading: false, }); -const formatNumber = (value: number) => - new Intl.NumberFormat("fa-IR").format(value); +const formatNumber = (value: number) => new Intl.NumberFormat("fa-IR").format(value); -const defaultFormatFieldValue = (value: ParserAnswerValue) => { - if (value === null || value === undefined || value === "") { - return "—"; - } - - if (typeof value === "number") { - return formatNumber(value); - } - - if (typeof value === "boolean") { - return value ? "بله" : "خیر"; - } +const defaultFormatFieldValue = (value: FertilizationPlanAnswerValue) => { + if (value === null || value === undefined || value === "") return "—"; + if (typeof value === "number") return formatNumber(value); + if (typeof value === "boolean") return value ? "بله" : "خیر"; return value; }; -const formatNumberWithUnit = ( - value: number | null | undefined, - unit: string, -) => { - if (value === null || value === undefined) { - return null; - } +const formatNumberWithUnit = (value: number | null | undefined, unit: string) => { + if (value === null || value === undefined) return null; return `${formatNumber(value)} ${unit}`; }; -const getStatusMeta = (status?: ParserResponse["status"]) => { +const getStatusMeta = (status?: FertilizationPlanParserResult["status"]) => { if (status === "completed") { return { label: "آماده اجرا", @@ -177,9 +150,7 @@ const getStatusMeta = (status?: ParserResponse["status"]) => { }; const extractErrorMessage = (error: unknown, fallback: string) => { - if (typeof error !== "object" || error === null) { - return fallback; - } + if (typeof error !== "object" || error === null) return fallback; if ( "details" in error && @@ -192,10 +163,7 @@ const extractErrorMessage = (error: unknown, fallback: string) => { const data = error.details.data as Record; const nonFieldErrors = data.non_field_errors; - if ( - Array.isArray(nonFieldErrors) && - typeof nonFieldErrors[0] === "string" - ) { + if (Array.isArray(nonFieldErrors) && typeof nonFieldErrors[0] === "string") { return nonFieldErrors[0]; } } @@ -207,15 +175,23 @@ const extractErrorMessage = (error: unknown, fallback: string) => { return fallback; }; -const createJsonSnapshot = (plan: unknown) => { - if (!plan) { - return "{}"; - } +const createJsonSnapshot = (plan: unknown) => JSON.stringify(plan ?? {}, null, 2); - return JSON.stringify(plan, null, 2); +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 getFertilizationFieldValue = (plan: unknown, field: string) => { +const getFieldValue = (plan: unknown, field: string) => { const data = plan as FertilizationPlanData | null | undefined; if (!data) return null; @@ -237,30 +213,50 @@ const getFertilizationFieldValue = (plan: unknown, field: string) => { return null; }; -const getIrrigationFieldValue = (plan: unknown, field: string) => { - const data = plan as IrrigationPlanData | null | undefined; +const getPrimaryMeta = (item: unknown): FieldMeta[] => { + const application = item as FertilizationPlanApplication | null; - if (!data) return null; - if (field === "crop_name") return data.crop_name; - if (field === "growth_stage") return data.growth_stage; - if (field === "irrigation_method") return data.irrigation_method; - if (field === "water_amount_per_event") return data.water_amount_per_event; - if (field === "duration_minutes") return data.duration_minutes; - if (field === "frequency_text") return data.frequency_text; - if (field === "interval_days") return data.interval_days; - if (field === "preferred_time_of_day") return data.preferred_time_of_day; - if (field === "start_date") return data.start_date; - if (field === "target_area") return data.target_area; + if (!application) return []; - return null; + return [ + { + key: "formula", + label: FERTILIZATION_FIELD_LABELS.formula, + value: application.formula, + }, + { + key: "amount", + label: FERTILIZATION_FIELD_LABELS.amount, + value: application.amount, + }, + { + key: "application_method", + label: FERTILIZATION_FIELD_LABELS.application_method, + value: application.application_method, + }, + { + key: "timing", + label: FERTILIZATION_FIELD_LABELS.timing, + value: application.timing, + }, + { + key: "interval_days", + label: FERTILIZATION_FIELD_LABELS.interval_days, + value: formatNumberWithUnit(application.interval_days, "روز"), + }, + { + key: "purpose", + label: FERTILIZATION_FIELD_LABELS.purpose, + value: application.purpose, + }, + ]; }; const buildNextAnswersState = ( - response: ParserResponse, + response: FertilizationPlanParserResult, previousAnswers: Record, - config: ParserConfig, -) => { - return response.questions.reduce>((acc, question) => { +) => + response.questions.reduce>((acc, question) => { const previousValue = previousAnswers[question.field]; if (previousValue) { @@ -268,10 +264,7 @@ const buildNextAnswersState = ( return acc; } - const extractedValue = config.getFieldValue( - response.collected_data, - question.field, - ); + const extractedValue = getFieldValue(response.collected_data, question.field); if (extractedValue !== null && extractedValue !== undefined) { acc[question.field] = String(extractedValue); @@ -279,323 +272,51 @@ const buildNextAnswersState = ( return acc; }, {}); -}; const normalizeAnswers = ( questions: ParserQuestion[], answers: Record, -) => { - return questions.reduce>( - (acc, question) => { - const rawValue = answers[question.field]?.trim(); +) => + questions.reduce>((acc, question) => { + const rawValue = answers[question.field]?.trim(); - if (!rawValue) { - return acc; - } + if (!rawValue) return acc; - if ( - question.field === "interval_days" || - question.field === "duration_minutes" - ) { - const parsed = Number(rawValue); - acc[question.field] = Number.isFinite(parsed) ? parsed : rawValue; - return acc; - } - - acc[question.field] = rawValue; + if (question.field === "interval_days") { + const parsed = Number(rawValue); + acc[question.field] = Number.isFinite(parsed) ? parsed : rawValue; return acc; - }, - {}, - ); -}; + } -const FERTILIZATION_FIELD_LABELS: Record = { - crop_name: "محصول", - growth_stage: "مرحله رشد", - objective: "هدف برنامه", - fertilizer_name: "نام کود", - formula: "فرمول کود", - amount: "مقدار مصرف", - application_method: "روش مصرف", - timing: "زمان بندی", - interval_days: "فاصله نوبت ها", - purpose: "هدف هر نوبت", -}; + acc[question.field] = rawValue; + return acc; + }, {}); -const IRRIGATION_FIELD_LABELS: Record = { - crop_name: "محصول", - growth_stage: "مرحله رشد", - irrigation_method: "روش آبیاری", - water_amount_per_event: "مقدار آب هر نوبت", - duration_minutes: "مدت هر نوبت", - frequency_text: "تناوب آبیاری", - interval_days: "فاصله آبیاری", - preferred_time_of_day: "زمان مناسب اجرا", - start_date: "زمان شروع", - target_area: "محدوده اجرا", -}; - -const PARSER_CONFIGS: Record = { - fertilization: { - key: "fertilization", - label: "کودهی", - badge: "Fertilization AI Planner", - icon: "tabler-flask-2", - heroTitle: "برنامه کودهی با ورودی متنی و خروجی JSON آماده اجرا", - heroDescription: - "متن آزاد کشاورز را بگیر، اگر داده ناقص بود سوال تکمیلی بپرس و در نهایت یک نسخه ساختاریافته و خوش خوان از برنامه کودهی تحویل بده.", - panelTitle: "اتاق فرمان برنامه کودهی", - panelDescription: - "هر چقدر متن طبیعی تر و نزدیک به زبان خود کشاورز باشد، API بهتر می تواند ساختار نهایی را استخراج کند.", - inputLabel: "متن آزاد برنامه کودهی", - inputPlaceholder: - "مثلا: برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.", - samplePrompts: [ - "برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.", - "برای ذرت از کود کامل استفاده می کنم اما فاصله بین نوبت ها را دقیق نمی دانم و می خواهم برنامه ام کامل شود.", - "برای گوجه فرنگی در شروع گلدهی یک برنامه کودهی دقیق با کود NPK و مصرف مرحله ای می خواهم.", - ], - fieldLabels: FERTILIZATION_FIELD_LABELS, - previewOrder: [ - "crop_name", - "growth_stage", - "objective", - "fertilizer_name", - "formula", - "amount", - "application_method", - "timing", - "interval_days", - "purpose", - ], - itemCountTitle: "نوبت های کودی", - primaryCardTitle: "نسخه پیشنهادی برای اولین نوبت کودی", - primaryFallbackTitle: "نام کود هنوز مشخص نیست", - finalCardTitle: "خروجی نهایی کودهی آماده تحویل", - finalCardDescription: - "اگر لازم بود همین JSON را برای ذخیره، اشتراک گذاری یا مرحله بعدی workflow استفاده کن.", - buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({ - ...(message ? { message } : {}), - ...(answers ? { answers } : {}), - ...(partialPlan ? { partial_plan: partialPlan } : {}), - ...(farmUuid ? { farm_uuid: farmUuid } : {}), - }), - submit: (payload) => - fertilizationPlanParserService.parseFromText( - payload as Parameters< - typeof fertilizationPlanParserService.parseFromText - >[0], - ), - getFieldValue: getFertilizationFieldValue, - formatFieldValue: (field, value) => { - if (field === "interval_days" && typeof value === "number") { - return `${formatNumber(value)} روز`; - } - - return defaultFormatFieldValue(value); - }, - getPrimaryItem: (plan) => - (plan as FertilizationPlanData | null | undefined)?.applications?.[0] ?? - null, - getPrimaryMeta: (item) => { - const application = item as FertilizationPlanApplication | null; - - if (!application) return []; - - return [ - { - key: "formula", - label: FERTILIZATION_FIELD_LABELS.formula, - value: application.formula, - }, - { - key: "amount", - label: FERTILIZATION_FIELD_LABELS.amount, - value: application.amount, - }, - { - key: "application_method", - label: FERTILIZATION_FIELD_LABELS.application_method, - value: application.application_method, - }, - { - key: "timing", - label: FERTILIZATION_FIELD_LABELS.timing, - value: application.timing, - }, - { - key: "interval_days", - label: FERTILIZATION_FIELD_LABELS.interval_days, - value: formatNumberWithUnit(application.interval_days, "روز"), - }, - { - key: "purpose", - label: FERTILIZATION_FIELD_LABELS.purpose, - value: application.purpose, - }, - ]; - }, - getPrimaryHeadline: (item) => - (item as FertilizationPlanApplication | null)?.fertilizer_name || - "نام کود هنوز مشخص نیست", - getItemCount: (plan) => - (plan as FertilizationPlanData | null | undefined)?.applications - ?.length ?? 0, - }, - irrigation: { - key: "irrigation", - label: "آبیاری", - badge: "Irrigation AI Planner", - icon: "tabler-droplet-half-2", - heroTitle: "برنامه آبیاری با ورودی متنی و خروجی JSON آماده اجرا", - heroDescription: - "برای آبیاری هم همان flow هوشمند را داشته باش: متن آزاد را بفرست، ابهام ها را کامل کن و یک برنامه ساختاریافته و قابل اجرا بگیر.", - panelTitle: "اتاق فرمان برنامه آبیاری", - panelDescription: - "جزئیات روش آبیاری، زمان بندی و مقدار آب را با زبان طبیعی بنویس تا API آن را به برنامه دقیق تبدیل کند.", - inputLabel: "متن آزاد برنامه آبیاری", - inputPlaceholder: - "مثلا: برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.", - samplePrompts: [ - "برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.", - "برای هندوانه هر دو روز یک بار آبیاری می کنم اما زمان شروع برنامه و محدوده اجرا را هنوز مشخص نکرده ام.", - "برای خیار گلخانه ای با تیپ آبیاری می کنم و می خواهم برنامه ام بر اساس زمان و حجم آب کامل شود.", - ], - fieldLabels: IRRIGATION_FIELD_LABELS, - previewOrder: [ - "crop_name", - "growth_stage", - "irrigation_method", - "water_amount_per_event", - "duration_minutes", - "frequency_text", - "interval_days", - "preferred_time_of_day", - "start_date", - "target_area", - ], - itemCountTitle: "بخش های آبیاری", - primaryCardTitle: "چکیده اجرای برنامه آبیاری", - primaryFallbackTitle: "روش آبیاری هنوز مشخص نیست", - finalCardTitle: "خروجی نهایی آبیاری آماده تحویل", - finalCardDescription: - "از همین JSON می توانی برای نمایش، ذخیره یا ادامه workflow آبیاری استفاده کنی.", - buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({ - ...(message ? { message } : {}), - ...(answers ? { answers } : {}), - ...(partialPlan ? { partial_plan: partialPlan } : {}), - ...(farmUuid ? { farm_uuid: farmUuid } : {}), - }), - submit: (payload) => - irrigationPlanParserService.parseFromText( - payload as Parameters< - typeof irrigationPlanParserService.parseFromText - >[0], - ), - getFieldValue: getIrrigationFieldValue, - formatFieldValue: (field, value) => { - if (field === "duration_minutes" && typeof value === "number") { - return `${formatNumber(value)} دقیقه`; - } - - if (field === "interval_days" && typeof value === "number") { - return `${formatNumber(value)} روز`; - } - - return defaultFormatFieldValue(value); - }, - getPrimaryItem: (plan) => plan ?? null, - getPrimaryMeta: (item) => { - const data = item as IrrigationPlanData | null; - - if (!data) return []; - - return [ - { - key: "irrigation_method", - label: IRRIGATION_FIELD_LABELS.irrigation_method, - value: data.irrigation_method, - }, - { - key: "water_amount_per_event", - label: IRRIGATION_FIELD_LABELS.water_amount_per_event, - value: data.water_amount_per_event, - }, - { - key: "duration_minutes", - label: IRRIGATION_FIELD_LABELS.duration_minutes, - value: formatNumberWithUnit(data.duration_minutes, "دقیقه"), - }, - { - key: "frequency_text", - label: IRRIGATION_FIELD_LABELS.frequency_text, - value: data.frequency_text, - }, - { - key: "preferred_time_of_day", - label: IRRIGATION_FIELD_LABELS.preferred_time_of_day, - value: data.preferred_time_of_day, - }, - { - key: "target_area", - label: IRRIGATION_FIELD_LABELS.target_area, - value: data.target_area, - }, - ]; - }, - getPrimaryHeadline: (item) => - (item as IrrigationPlanData | null)?.irrigation_method || - "روش آبیاری هنوز مشخص نیست", - getItemCount: (plan) => { - const data = plan as IrrigationPlanData | null | undefined; - - if (!data) return 0; - - return 1 + (data.trigger_conditions?.length ?? 0); - }, - }, -}; - -const FertilizationPlanParserPage = ({ - initialTab = "fertilization", - enabledTabs = ["fertilization", "irrigation"], -}: FertilizationPlanParserPageProps) => { +const FertilizationPlanParserPage = (_props: FertilizationPlanParserPageProps) => { const theme = useTheme(); const { farmHub } = useFarmHub(); const farmUuid = farmHub?.farm_uuid; - const availableTabs = enabledTabs.filter( - (tab): tab is ParserTabKey => tab in PARSER_CONFIGS, - ); - const resolvedInitialTab = availableTabs.includes(initialTab) - ? initialTab - : availableTabs[0] ?? "fertilization"; - const singleTabMode = availableTabs.length === 1; + const [state, setState] = useState(createInitialState()); + const [irrigationDialogOpen, setIrrigationDialogOpen] = useState(false); + const [irrigationPlans, setIrrigationPlans] = useState([]); + const [selectedIrrigationPlanId, setSelectedIrrigationPlanId] = useState< + number | string | null + >(null); + const [irrigationLoading, setIrrigationLoading] = useState(false); + const [irrigationError, setIrrigationError] = useState(null); - const [activeTab, setActiveTab] = useState(resolvedInitialTab); - const [tabStates, setTabStates] = useState>({ - fertilization: createInitialTabState(), - irrigation: createInitialTabState(), - }); + const statusMeta = getStatusMeta(state.response?.status); + const planPreview = state.response?.final_plan ?? state.response?.collected_data ?? null; + const primaryItem = + (planPreview as FertilizationPlanData | null | undefined)?.applications?.[0] ?? null; + const currentQuestion = state.response?.questions[state.activeQuestion] ?? null; + const planData = planPreview as FertilizationPlanData | null | undefined; - const config = PARSER_CONFIGS[activeTab]; - const currentState = tabStates[activeTab]; - const statusMeta = getStatusMeta(currentState.response?.status); - const planPreview = - currentState.response?.final_plan ?? - currentState.response?.collected_data ?? - null; - const primaryItem = config.getPrimaryItem(planPreview); - const currentQuestion = - currentState.response?.questions[currentState.activeQuestion] ?? null; - - const completionValue = currentState.response + const completionValue = state.response ? Math.max( 20, Math.round( - ((config.previewOrder.length - - currentState.response.missing_fields.length) / - config.previewOrder.length) * - 100, + ((PREVIEW_ORDER.length - state.response.missing_fields.length) / PREVIEW_ORDER.length) * 100, ), ) : 18; @@ -603,58 +324,41 @@ const FertilizationPlanParserPage = ({ const statCards = useMemo( () => [ { - stats: currentState.response?.status_fa ?? "شروع نشده", + stats: state.response?.status_fa ?? "شروع نشده", title: "وضعیت پردازش", avatarIcon: statusMeta.icon, avatarColor: statusMeta.color as ThemeColor, }, { - stats: formatNumber(currentState.response?.missing_fields.length ?? 0), + stats: formatNumber(state.response?.missing_fields.length ?? 0), title: "فیلدهای ناقص", avatarIcon: "tabler-help-circle", avatarColor: "warning" as ThemeColor, }, { - stats: formatNumber(config.getItemCount(planPreview)), - title: config.itemCountTitle, - avatarIcon: config.icon, + stats: formatNumber( + (planPreview as FertilizationPlanData | null | undefined)?.applications?.length ?? 0, + ), + title: "نوبت های کودی", + avatarIcon: "tabler-flask-2", avatarColor: "success" as ThemeColor, }, ], - [ - config, - currentState.response?.missing_fields.length, - currentState.response?.status_fa, - planPreview, - statusMeta.color, - statusMeta.icon, - ], + [planPreview, state.response?.missing_fields.length, state.response?.status_fa, statusMeta.color, statusMeta.icon], ); - const updateTabState = ( - tab: ParserTabKey, - updater: Partial | ((previous: TabState) => TabState), - ) => { - setTabStates((previous) => ({ - ...previous, - [tab]: - typeof updater === "function" - ? updater(previous[tab]) - : { ...previous[tab], ...updater }, - })); + const updateState = (updater: Partial | ((previous: ParserState) => ParserState)) => { + setState((previous) => + typeof updater === "function" ? updater(previous) : { ...previous, ...updater }, + ); }; - const handleReset = (tab: ParserTabKey) => { - updateTabState(tab, createInitialTabState()); + const handleReset = () => { + setState(createInitialState()); }; - const submitRequest = async ( - tab: ParserTabKey, - payload: Record, - ) => { - const tabConfig = PARSER_CONFIGS[tab]; - - updateTabState(tab, (previous) => ({ + const submitRequest = async (payload: Record) => { + updateState((previous) => ({ ...previous, loading: true, requestError: null, @@ -662,969 +366,636 @@ const FertilizationPlanParserPage = ({ })); try { - const nextResponse = await tabConfig.submit(payload); + const response = await fertilizationPlanParserService.parseFromText( + payload as Parameters[0], + ); - updateTabState(tab, (previous) => ({ + updateState((previous) => ({ ...previous, - response: nextResponse, - answers: buildNextAnswersState( - nextResponse, - previous.answers, - tabConfig, - ), + response, + answers: buildNextAnswersState(response, previous.answers), activeQuestion: 0, + loading: false, statusNote: - nextResponse.status === "completed" - ? `برنامه ${tabConfig.label} نهایی آماده شد و می توانی آن را با تیم مزرعه به اشتراک بگذاری.` - : "سیستم چند ابهام پیدا کرده؛ جواب ها را کامل کن تا JSON نهایی ساخته شود.", + response.status === "completed" + ? "خروجی نهایی برنامه کودهی آماده شد." + : "برای تکمیل برنامه، پاسخ سوال های تکمیلی را وارد کنید.", })); } catch (error) { - updateTabState(tab, (previous) => ({ - ...previous, - requestError: extractErrorMessage( - error, - `در ساخت برنامه ${tabConfig.label} مشکلی پیش آمد. دوباره تلاش کن.`, - ), - })); - } finally { - updateTabState(tab, (previous) => ({ + updateState((previous) => ({ ...previous, loading: false, + requestError: extractErrorMessage( + error, + "ارسال درخواست برنامه کودهی انجام نشد.", + ), })); } }; - const handleGeneratePlan = async () => { - const trimmedMessage = currentState.message.trim(); + const handleGeneratePlan = () => { + const message = state.message.trim(); - if (!trimmedMessage) { - updateTabState(activeTab, (previous) => ({ - ...previous, - requestError: `اول متن برنامه ${config.label} را بنویس تا تحلیل را شروع کنیم.`, - })); + if (!message) { + updateState({ requestError: "ابتدا متن برنامه کودهی را وارد کنید." }); return; } - await submitRequest( - activeTab, - config.buildPayload({ - message: trimmedMessage, - farmUuid, - }), - ); + submitRequest({ + message, + ...(farmUuid ? { farm_uuid: farmUuid } : {}), + }); }; - const handleSubmitAnswers = async () => { - if (!currentState.response) { - return; - } + const handleSubmitAnswers = () => { + if (!state.response) return; - const normalizedAnswers = normalizeAnswers( - currentState.response.questions, - currentState.answers, - ); - - const unansweredQuestion = currentState.response.questions.find( - (question) => { - const value = normalizedAnswers[question.field]; - - return value === undefined || value === null || value === ""; - }, - ); - - if (unansweredQuestion) { - updateTabState(activeTab, (previous) => ({ - ...previous, - requestError: `پاسخ سوال «${config.fieldLabels[unansweredQuestion.field] ?? unansweredQuestion.question}» هنوز کامل نشده است.`, - })); - return; - } - - await submitRequest( - activeTab, - config.buildPayload({ - answers: normalizedAnswers, - partialPlan: currentState.response.collected_data, - farmUuid, - }), - ); + submitRequest({ + answers: normalizeAnswers(state.response.questions, state.answers), + partial_plan: state.response.collected_data, + ...(farmUuid ? { farm_uuid: farmUuid } : {}), + }); }; const handleCopyJson = async () => { - if ( - !planPreview || - typeof navigator === "undefined" || - !navigator.clipboard - ) { - updateTabState(activeTab, (previous) => ({ - ...previous, - statusNote: "کپی خودکار روی این مرورگر در دسترس نیست.", - })); + if (typeof navigator === "undefined" || !navigator.clipboard) return; + + await navigator.clipboard.writeText(createJsonSnapshot(planPreview)); + updateState({ statusNote: "خروجی JSON کپی شد." }); + }; + + const handleOpenIrrigationDialog = async () => { + setIrrigationDialogOpen(true); + + if (!farmUuid) { + setIrrigationError("ابتدا مزرعه فعال را انتخاب کنید."); + setIrrigationPlans([]); + setSelectedIrrigationPlanId(null); return; } - await navigator.clipboard.writeText(createJsonSnapshot(planPreview)); + setIrrigationLoading(true); + setIrrigationError(null); - updateTabState(activeTab, (previous) => ({ - ...previous, - statusNote: `نسخه JSON برنامه ${config.label} در کلیپ بورد کپی شد.`, - })); + try { + 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", + })); + + setIrrigationPlans(items); + setSelectedIrrigationPlanId(items[0]?.id ?? null); + } catch (error) { + setIrrigationError( + extractErrorMessage(error, "دریافت لیست برنامه‌های آبیاری انجام نشد."), + ); + setIrrigationPlans([]); + setSelectedIrrigationPlanId(null); + } finally { + setIrrigationLoading(false); + } }; return ( - - - - - + + + + + + /> + + برنامه کودهی با ورودی متنی و خروجی JSON آماده اجرا + + + متن آزاد کشاورز را بگیر، اگر داده ناقص بود سوال تکمیلی بپرس و در نهایت یک نسخه + ساختاریافته و خوش خوان از برنامه کودهی تحویل بده. + + + + + - - - - + + + اتاق فرمان برنامه کودهی + + + هر چقدر متن طبیعی تر و نزدیک به زبان خود کشاورز باشد، API بهتر می تواند + ساختار نهایی را استخراج کند. + + + updateState((previous) => ({ ...previous, message: SAMPLE_PROMPTS[0] })), + }, + }, + { + text: "کپی JSON", + icon: "tabler-copy", + menuItemProps: { + onClick: handleCopyJson, + }, + }, + { + text: "شروع دوباره", + icon: "tabler-rotate-clockwise-2", + menuItemProps: { + onClick: handleReset, + }, + }, + ]} + /> + + + {!farmUuid ? ( + + مزرعه فعالی پیدا نشد. صفحه هنوز کار می کند، اما اگر `farm_uuid` فعال باشد پاسخ + API دقیق تر می شود. + + ) : null} + + + updateState((previous) => ({ + ...previous, + message: event.target.value, + })) + } + /> + + + + نمونه های آماده برای شروع سریع + + + {SAMPLE_PROMPTS.map((prompt) => ( + updateState((previous) => ({ + ...previous, + message: prompt, + })) + } + variant="outlined" sx={{ - width: "fit-content", - color: "common.white", - border: `1px solid ${alpha(theme.palette.common.white, 0.26)}`, - backgroundColor: alpha( - theme.palette.common.white, - 0.12, - ), + justifyContent: "flex-start", + px: 1, + py: 2.75, + maxWidth: "100%", + height: "auto", + borderRadius: 3, + backgroundColor: alpha(theme.palette.success.main, 0.05), + "& .MuiChip-label": { + display: "block", + whiteSpace: "normal", + }, }} /> - - - {config.heroTitle} - - - {config.heroDescription} - - - - - - موتور تحلیل - - - Free-text parser + clarification flow - - - - - مزرعه فعال - - - {farmHub?.name || farmUuid || "بدون مزرعه فعال"} - - - - - - - - - - - - {singleTabMode - ? `جریان هوشمند ${config.label}` - : "دو جریان هوشمند، یک صفحه واحد"} - - - {singleTabMode - ? `ورودی متنی برنامه ${config.label} را بفرست، ابهام ها را کامل کن و خروجی JSON نهایی بگیر.` - : "بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با همان flow سوال های تکمیلی تا JSON نهایی پیش ببر."} - - - - - متن آزاد - - - تکمیل ابهام ها - - - برنامه نهایی - - - - - - - - - - - - - - - - - {!singleTabMode ? ( - setActiveTab(value)} - variant="fullWidth" - sx={{ - p: 1, - borderRadius: 4, - backgroundColor: alpha(theme.palette.primary.main, 0.05), - minHeight: 64, - "& .MuiTabs-indicator": { display: "none" }, - }} - > - {availableTabs.map((tabKey) => { - const tabConfig = PARSER_CONFIGS[tabKey]; - - return ( - } - iconPosition="start" - label={tabConfig.label} - sx={{ - minHeight: 54, - borderRadius: 3, - fontWeight: 700, - transition: "all 0.2s ease", - "&.Mui-selected": { - color: theme.palette.common.white, - background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.info.main} 100%)`, - boxShadow: `0 14px 32px ${alpha(theme.palette.primary.main, 0.24)}`, - }, - }} - /> - ); - })} - - ) : null} - - - - - - {config.panelTitle} - - - {config.panelDescription} - - - - updateTabState(activeTab, (previous) => ({ - ...previous, - message: config.samplePrompts[0], - })), - }, - }, - { - text: "کپی JSON", - icon: "tabler-copy", - menuItemProps: { - onClick: handleCopyJson, - }, - }, - { - text: "شروع دوباره", - icon: "tabler-rotate-clockwise-2", - menuItemProps: { - onClick: () => handleReset(activeTab), - }, - }, - ]} - /> - - - {!farmUuid && ( - - مزرعه فعالی پیدا نشد. صفحه هنوز کار می کند، اما اگر - `farm_uuid` فعال باشد پاسخ API دقیق تر می شود. - - )} - - - updateTabState(activeTab, (previous) => ({ - ...previous, - message: event.target.value, - })) - } - /> - - - - نمونه های آماده برای شروع سریع - - - {config.samplePrompts.map((prompt) => ( - - updateTabState(activeTab, (previous) => ({ - ...previous, - message: prompt, - })) - } - variant="outlined" - sx={{ - justifyContent: "flex-start", - px: 1, - py: 2.75, - maxWidth: "100%", - height: "auto", - borderRadius: 3, - backgroundColor: alpha( - theme.palette.success.main, - 0.05, - ), - "& .MuiChip-label": { - display: "block", - whiteSpace: "normal", - }, - }} - /> - ))} - - - - - - + ))} + - {currentState.loading && ( - - )} - - {currentState.requestError && ( - {currentState.requestError} - )} - {currentState.statusNote && ( - {currentState.statusNote} - )} - - {currentState.response && ( - - - - - - - - - - - {currentState.response.status_fa} - - - {currentState.response.summary} - - - - - - - {currentState.response.status === - "needs_clarification" && - currentState.response.questions.length > 0 && - currentQuestion && ( - - - - - مرحله تکمیل ابهام ها - - - سوال ها را یکی یکی جلو ببر؛ بعد از تکمیل، همان - endpoint با `answers` و `partial_plan` دوباره - صدا زده می شود. - - - - {currentState.response.questions.map( - (question) => ( - - - {config.fieldLabels[question.field] ?? - question.field} - - - ), - )} - - - - - - - - - - - سوال{" "} - {formatNumber( - currentState.activeQuestion + 1, - )}{" "} - از{" "} - {formatNumber( - currentState.response.questions - .length, - )} - - - {config.fieldLabels[ - currentQuestion.field - ] ?? currentQuestion.field} - - - - - - {currentQuestion.question} - - - {currentQuestion.rationale} - - - - updateTabState( - activeTab, - (previous) => ({ - ...previous, - answers: { - ...previous.answers, - [currentQuestion.field]: - event.target.value, - }, - }), - ) - } - placeholder="پاسخ را اینجا بنویس..." - /> - - - - - - - - - - - - )} - - - - )} + + + - - + + {state.loading ? : null} + {state.requestError ? {state.requestError} : null} + {state.statusNote ? {state.statusNote} : null} + + {state.response ? ( + + + + + + + + + + + {state.response.status_fa} + + {state.response.summary} + + + + + + {state.response.status === "needs_clarification" && + state.response.questions.length > 0 && + currentQuestion ? ( + + + + + مرحله تکمیل ابهام ها + + + سوال ها را یکی یکی جلو ببر؛ بعد از تکمیل، همان endpoint با `answers` + و `partial_plan` دوباره صدا زده می شود. + + + + {state.response.questions.map((question) => ( + + + {FERTILIZATION_FIELD_LABELS[question.field] ?? question.field} + + + ))} + + + + + + + + + + + سوال {formatNumber(state.activeQuestion + 1)} از{" "} + {formatNumber(state.response.questions.length)} + + + {FERTILIZATION_FIELD_LABELS[currentQuestion.field] ?? + currentQuestion.field} + + + + + + {currentQuestion.question} + + + {currentQuestion.rationale} + + + + updateState((previous) => ({ + ...previous, + answers: { + ...previous.answers, + [currentQuestion.field]: event.target.value, + }, + })) + } + placeholder="پاسخ را اینجا بنویس..." + /> + + + + + + + + + + + + ) : null} + + + + ) : null} + + + - - - {statCards.map((card) => ( - - - - ))} - + + + {statCards.map((card) => ( + + + + ))} + - - - - - پیش نمایش زنده داده ساختاریافته - - - این بخش نشان می دهد سیستم تا این لحظه چه چیزهایی را فهمیده - است. - + + + + + پیش نمایش زنده داده ساختاریافته + + + این بخش نشان می دهد سیستم تا این لحظه چه چیزهایی را فهمیده است. + + + + + + درصد تکمیل برنامه + {formatNumber(completionValue)}٪ + + - - - - - درصد تکمیل برنامه + + {planPreview ? ( + + {PREVIEW_ORDER.map((field) => { + const value = getFieldValue(planPreview, field); + + return ( + + + {FERTILIZATION_FIELD_LABELS[field] ?? field} + + + {field === "interval_days" && typeof value === "number" + ? `${formatNumber(value)} روز` + : defaultFormatFieldValue(value)} + + + ); + })} + + ) : ( + + هنوز خروجی ای نداریم. متن را ارسال کن تا `collected_data` و سپس `final_plan` + اینجا شکل بگیرد. + + )} + + + + + {primaryItem !== null && primaryItem !== undefined ? ( + + + + + + + نسخه پیشنهادی برای اولین نوبت کودی - {formatNumber(completionValue)}٪ + {( + primaryItem as FertilizationPlanApplication | null + )?.fertilizer_name || "نام کود هنوز مشخص نیست"} - - - - {planPreview ? ( - - {config.previewOrder.map((field) => { - const value = config.getFieldValue(planPreview, field); - - return ( - - - {config.fieldLabels[field] ?? field} - - - {config.formatFieldValue(field, value)} - - - ); - })} - - ) : ( - - هنوز خروجی ای نداریم. متن را ارسال کن تا collected_data و - سپس final_plan اینجا شکل بگیرد. - - )} - - - - - {primaryItem !== null && primaryItem !== undefined && ( - - - - - - - {config.primaryCardTitle} - - - {config.getPrimaryHeadline(primaryItem) || - config.primaryFallbackTitle} - - - - - - - {config.getPrimaryMeta(primaryItem).map((item) => ( - - - - {item.label} - - - {item.value || "—"} - - - - ))} - - - - - )} - - {currentState.response?.final_plan && ( - - - - - - - {config.finalCardTitle} - - - {config.finalCardDescription} - - + + - - - {createJsonSnapshot(currentState.response.final_plan)} - - - - - )} - + + + + + {getPrimaryMeta(primaryItem).map((meta) => ( + + + + {meta.label} + + {meta.value || "—"} + + + ))} + + + + + ) : null} + + + + + + خروجی نهایی کودهی آماده تحویل + + + اگر لازم بود همین JSON را برای ذخیره، اشتراک گذاری یا مرحله بعدی workflow + استفاده کن. + + + + {createJsonSnapshot(planPreview)} + + + + - + + setIrrigationDialogOpen(false)} + title="جزییات برنامه کودهی" + description="برای تکمیل اجرای این برنامه کودهی، یکی از برنامه‌های آبیاری مرتبط را انتخاب کنید." + currentPlanName={planData?.crop_name || state.response?.summary || "برنامه کودهی"} + currentPlanStatus={ + state.response?.status === "completed" ? "برنامه فعال" : "آماده برای فعال‌سازی" + } + currentPlanOutput={`منبع: ${state.response ? "متن آزاد کاربر" : "—"}`} + summaryItems={[ + { label: "محصول نهایی", value: planData?.crop_name || "—" }, + { label: "تاریخ ایجاد", value: formatDate(new Date().toISOString()) }, + { + label: "جزئیات", + value: + (primaryItem as FertilizationPlanApplication | null)?.fertilizer_name || + planData?.objective || + "—", + }, + ]} + relatedTitle="برنامه‌های آبیاری پیشنهادی" + relatedDescription="از بین برنامه‌های زیر، مناسب‌ترین برنامه آبیاری را برای این برنامه کودی انتخاب کنید." + relatedPlans={irrigationLoading ? [] : irrigationPlans} + selectedRelatedPlanId={selectedIrrigationPlanId} + onSelectRelatedPlan={(planId) => setSelectedIrrigationPlanId(planId)} + onConfirm={() => setIrrigationDialogOpen(false)} + /> + + {irrigationDialogOpen && irrigationLoading ? : null} + {irrigationDialogOpen && irrigationError ? {irrigationError} : null} + ); }; diff --git a/src/views/dashboards/farm/planSelector/RelatedPlanSelector.tsx b/src/views/dashboards/farm/planSelector/RelatedPlanSelector.tsx index bceb865..e7507f2 100644 --- a/src/views/dashboards/farm/planSelector/RelatedPlanSelector.tsx +++ b/src/views/dashboards/farm/planSelector/RelatedPlanSelector.tsx @@ -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; };