From ffac63664864b23dca4b615c39e9e8d4813cf022 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Tue, 28 Apr 2026 14:48:31 +0330 Subject: [PATCH] UPDATE --- .../SmartIrrigationRecommendation.tsx | 2507 ++++++++++------- 1 file changed, 1484 insertions(+), 1023 deletions(-) diff --git a/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx index 004a833..9736c3d 100644 --- a/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx +++ b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx @@ -1,53 +1,40 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; +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 Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; import CircularProgress from "@mui/material/CircularProgress"; -import Alert from "@mui/material/Alert"; -import AlertTitle from "@mui/material/AlertTitle"; -import Checkbox from "@mui/material/Checkbox"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import FormGroup from "@mui/material/FormGroup"; -import Tab from "@mui/material/Tab"; -import Tabs from "@mui/material/Tabs"; -import Timeline from "@mui/lab/Timeline"; -import TimelineConnector from "@mui/lab/TimelineConnector"; -import TimelineContent from "@mui/lab/TimelineContent"; -import TimelineDot from "@mui/lab/TimelineDot"; -import TimelineItem from "@mui/lab/TimelineItem"; -import TimelineSeparator from "@mui/lab/TimelineSeparator"; -import { useTheme, alpha } from "@mui/material/styles"; -import { - CalendarDays, - Download, - Droplets, - Save, - Sprout, -} from "lucide-react"; -import { - Bar, - BarChart, - CartesianGrid, - Legend, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { useFarmHub } from "@/hooks/useFarmHub"; +import Collapse from "@mui/material/Collapse"; +import Drawer from "@mui/material/Drawer"; +import IconButton from "@mui/material/IconButton"; +import LinearProgress from "@mui/material/LinearProgress"; +import Paper from "@mui/material/Paper"; +import Slider from "@mui/material/Slider"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import Tooltip from "@mui/material/Tooltip"; +import Step from "@mui/material/Step"; +import StepContent from "@mui/material/StepContent"; +import StepLabel from "@mui/material/StepLabel"; +import Stepper from "@mui/material/Stepper"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; import type { CropOption, - IrrigationPlan, - WaterBalance, + IrrigationRecommendationResult, + WaterBalanceDailyEntry, } from "@/libs/api/services/irrigationRecommendationService"; -import { irrigationRecommendationService } from "@/libs/api/services/irrigationRecommendationService"; -import { isRecommendationTaskRunning } from "@/libs/api/services/recommendationTask"; -import { selectedPlantsService } from "@/libs/api/services/selectedPlantsService"; const GROWTH_STAGE_LABELS: Record = { initial: "شروع رشد", @@ -64,6 +51,7 @@ const PLANT_ICON_MAP: Record = { saffron: "tabler-flower-2", canola: "tabler-leaf", vegetables: "tabler-carrot", + cucumber: "tabler-leaf", }; const GROWTH_STAGE_ICON_MAP: Record = { @@ -74,1048 +62,1481 @@ const GROWTH_STAGE_ICON_MAP: Record = { maturity: "tabler-basket", }; +type GrowthStage = { + id: string; + icon: string; + label?: string; +}; + +type RecommendationSection = { + title: string; + icon: string; + type: "schedule" | "warning" | "tip" | "method"; + content: string; +}; + +type ScheduleStep = { + step_number: number; + title: string; + description: string; +}; + +type AlternativePlan = { + code: string; + title: string; + irrigation_method: string; + description: string; +}; + +type MockIrrigationRecommendation = IrrigationRecommendationResult & { + generated_at: string; + recommendation_title: string; + recommendation_subtitle: string; + primary_method: { + label: string; + pressure: string; + target_moisture: number; + coverage: string; + }; + usage_summary: { + duration_label: string; + frequency_label: string; + preferred_window: string; + area_basis_mm: number; + area_basis_liters: number; + }; + timeline: ScheduleStep[]; + alternative_plans: AlternativePlan[]; + sections: RecommendationSection[]; +}; + +type MockHistoryItem = { + recommendation_uuid: string; + plant_name: string; + growth_stage: string; + irrigation_method: string; + status: "pending_confirmation" | "in_progress" | "completed"; + status_label: string; + requested_at: string; + result: MockIrrigationRecommendation; +}; + +type HistoryPagination = { + page: number; + page_size: number; + total_pages: number; + total_items: number; + has_next: boolean; + has_previous: boolean; + next: string | null; + previous: string | null; +}; + +type DetailsSheetState = { + isOpen: boolean; + title: string; + content: string; + type: "balance" | "alternative" | "section"; +}; + const formatStageLabel = (stage: string) => GROWTH_STAGE_LABELS[stage] ?? stage .split(/[_-]/) .filter(Boolean) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); const getPlantIcon = (icon: string) => PLANT_ICON_MAP[icon] ?? "tabler-leaf"; const getGrowthStageIcon = (stage: string) => GROWTH_STAGE_ICON_MAP[stage] ?? "tabler-circle-dot"; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const getErrorMessage = (error: unknown, fallback: string) => - typeof error === "object" && - error !== null && - "message" in error && - typeof error.message === "string" - ? error.message - : fallback; +const formatNumber = (value: number) => + new Intl.NumberFormat("fa-IR", { maximumFractionDigits: 2 }).format(value); -type FertilizerPlan = { - generated: boolean; - crop: string; - status: string; - alerts: Array<{ - severity: "success" | "info" | "warning" | "error"; - title: string; - message: string; - }>; - nutrients: Array<{ - name: string; - current: number; - target: number; - }>; - recommendedFertilizers: Array<{ - id: number; - name: string; - type: string; - dosage: string; - method: string; - }>; - stages: Array<{ - id: number; - phase: string; - time: string; - summary: string; - completed: boolean; - }>; +const formatDateTime = (value: string) => { + const date = new Date(value); + + if (Number.isNaN(date.getTime())) return value; + + return new Intl.DateTimeFormat("fa-IR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(date); }; -const MOCK_FERTILIZER_PLAN: FertilizerPlan = { - generated: true, - crop: "Wheat", - status: "Plan ready", - alerts: [ - { - severity: "warning", - title: "Low Nitrogen", - message: "Soil N is below optimal levels.", - }, - { - severity: "error", - title: "High Salinity", - message: "EC levels are critical, avoid high-salt fertilizers.", - }, - ], - nutrients: [ - { name: "Nitrogen (N)", current: 30, target: 80 }, - { name: "Phosphorus (P)", current: 45, target: 60 }, - { name: "Potassium (K)", current: 70, target: 90 }, - ], - recommendedFertilizers: [ - { - id: 1, - name: "Urea", - type: "46-0-0", - dosage: "50 kg/ha", - method: "Soil Application", - }, - { - id: 2, - name: "Potassium Sulfate", - type: "0-0-50", - dosage: "25 kg/ha", - method: "Fertigation", - }, - { - id: 3, - name: "Micronutrient Mix", - type: "Liquid", - dosage: "2 L/ha", - method: "Foliar Spray", - }, - ], - stages: [ - { - id: 1, - phase: "Vegetative Growth", - time: "Week 2 - 4", - summary: "High Nitrogen application", - completed: true, - }, - { - id: 2, - phase: "Flowering", - time: "Week 5 - 7", - summary: "Switch to Phosphorus focus", - completed: false, - }, - { - id: 3, - phase: "Fruiting / Maturation", - time: "Week 8 - 10", - summary: "Potassium boost for fruit size", - completed: false, - }, - ], +const getStatusChipColor = ( + status: string, +): "warning" | "info" | "default" => { + if (status === "in_progress") return "info"; + if (status === "completed") return "default"; + + return "warning"; }; +const MOCK_CROP_OPTIONS: CropOption[] = [ + { + id: "wheat", + name: "گندم", + icon: getPlantIcon("wheat"), + growthStages: ["initial", "vegetative", "flowering", "maturity"], + }, + { + id: "corn", + name: "ذرت", + icon: getPlantIcon("corn"), + growthStages: ["initial", "vegetative", "flowering", "fruiting"], + }, + { + id: "cucumber", + name: "خیار", + icon: getPlantIcon("cucumber"), + growthStages: ["vegetative", "flowering", "fruiting"], + }, +]; + +const buildMockRecommendation = ( + cropName: string, + stageLabel: string, +): MockIrrigationRecommendation => ({ + generated_at: "2025-02-12T06:30:00Z", + recommendation_title: `نسخه آبیاری هوشمند ${cropName}`, + recommendation_subtitle: `پیشنهاد ویژه برای مرحله ${stageLabel}`, + status: "completed", + plan: { + frequencyPerWeek: 4, + durationMinutes: 38, + bestTimeOfDay: "05:30 تا 08:00 صبح", + moistureLevel: 72, + warning: + "با توجه به افزایش دمای بعدازظهر، از آبیاری بین ساعات 12 تا 16 خودداری شود.", + }, + raw_response: + "Mock irrigation response generated locally for UI development without API connection.", + water_balance: { + active_kc: 0.93, + crop_profile: { + kc_initial: 0.55, + kc_mid: 1.05, + kc_end: 0.78, + }, + daily: [ + { + forecast_date: "2025-02-12", + et0_mm: 5.4, + etc_mm: 4.9, + effective_rainfall_mm: 0, + gross_irrigation_mm: 17, + irrigation_timing: "05:30 - 07:00", + }, + { + forecast_date: "2025-02-13", + et0_mm: 5.1, + etc_mm: 4.6, + effective_rainfall_mm: 1.2, + gross_irrigation_mm: 15, + irrigation_timing: "06:00 - 07:20", + }, + { + forecast_date: "2025-02-14", + et0_mm: 5.8, + etc_mm: 5.1, + effective_rainfall_mm: 0, + gross_irrigation_mm: 18, + irrigation_timing: "05:20 - 07:10", + }, + ], + }, + primary_method: { + label: "آبیاری قطره ای نواری", + pressure: "1.2 تا 1.5 بار", + target_moisture: 78, + coverage: "پوشش یکنواخت برای ریشه فعال در عمق 25 تا 35 سانتی متر", + }, + usage_summary: { + duration_label: "38 دقیقه در هر نوبت", + frequency_label: "4 نوبت در هفته", + preferred_window: "صبح زود قبل از تابش مستقیم", + area_basis_mm: 17, + area_basis_liters: 170000, + }, + timeline: [ + { + step_number: 1, + title: "بررسی فشار و یکنواختی خروجی", + description: + "پیش از شروع، فشار ابتدای لاین و انتهای لاین را چک کنید تا اختلاف بیشتر از 0.2 بار نباشد.", + }, + { + step_number: 2, + title: "اجرای آبیاری نوبت اول", + description: + "در بازه صبح، 38 دقیقه آبیاری انجام شود تا رطوبت ناحیه ریشه به 78 درصد ظرفیت مزرعه برسد.", + }, + { + step_number: 3, + title: "بازبینی رطوبت خاک", + description: + "6 ساعت بعد از آبیاری، رطوبت سنج یا نمونه دستی خاک برای جلوگیری از آبیاری بیش از حد کنترل شود.", + }, + ], + alternative_plans: [ + { + code: "alt-drip-light", + title: "برنامه سبک برای روزهای ابری", + irrigation_method: "قطره ای با دبی کمتر", + description: + "در روزهای ابری یا کاهش تبخیر، مدت آبیاری را به 28 دقیقه کاهش دهید و نوبت چهارم را حذف کنید.", + }, + { + code: "alt-split-cycle", + title: "برنامه دو مرحله ای", + irrigation_method: "آبیاری دو پالس", + description: + "برای خاک های سنگین، آبیاری را به دو نوبت 20 دقیقه ای در صبح و عصر تقسیم کنید تا رواناب کاهش یابد.", + }, + ], + sections: [ + { + title: "نسخه اصلی آبیاری", + icon: "tabler-droplet-half-2", + type: "schedule", + content: + "در این مرحله، حفظ رطوبت یکنواخت اهمیت بیشتری از حجم یکجای آب دارد؛ بنابراین آبیاری کوتاه و منظم پیشنهاد می شود.", + }, + { + title: "هشدار تبخیر بالا", + icon: "tabler-alert-triangle", + type: "warning", + content: + "اگر سرعت باد بیشتر از 20 کیلومتر بر ساعت شد، شروع آبیاری را به ساعات خنک تر منتقل کنید.", + }, + { + title: "نکته بهره وری", + icon: "tabler-bulb", + type: "tip", + content: + "شست وشوی فیلترها در ابتدای هر هفته می تواند یکنواختی آبیاری را تا 12 درصد بهتر کند.", + }, + { + title: "روش اجرایی", + icon: "tabler-route", + type: "method", + content: + "ابتدا بلوک های جنوبی، سپس بلوک های مرکزی و در پایان نواحی کم شیب آبیاری شوند تا افت فشار مدیریت شود.", + }, + ], +}); + +const MOCK_HISTORY_SOURCE: MockHistoryItem[] = [ + { + recommendation_uuid: "irr-rec-001", + plant_name: "گندم", + growth_stage: "vegetative", + irrigation_method: "قطره ای نواری", + status: "completed", + status_label: "آماده اجرا", + requested_at: "2025-02-12T06:30:00Z", + result: buildMockRecommendation("گندم", "رشد رویشی"), + }, + { + recommendation_uuid: "irr-rec-002", + plant_name: "ذرت", + growth_stage: "flowering", + irrigation_method: "تیپ با دو پالس", + status: "in_progress", + status_label: "در حال پایش", + requested_at: "2025-02-09T05:45:00Z", + result: buildMockRecommendation("ذرت", "گلدهی"), + }, + { + recommendation_uuid: "irr-rec-003", + plant_name: "خیار", + growth_stage: "fruiting", + irrigation_method: "قطره ای سبک", + status: "pending_confirmation", + status_label: "نیازمند تایید", + requested_at: "2025-02-06T07:10:00Z", + result: buildMockRecommendation("خیار", "باردهی"), + }, +]; + export default function SmartIrrigationRecommendation() { const t = useTranslations("irrigation"); const theme = useTheme(); - const { farmHub } = useFarmHub(); - const farmUuid = farmHub?.farm_uuid; - const [cropOptions, setCropOptions] = useState([]); - const [configLoading, setConfigLoading] = useState(true); - const [configError, setConfigError] = useState(null); - const [selectedCrop, setSelectedCrop] = useState(null); - const [growthStages, setGrowthStages] = useState([]); - const [selectedGrowthStage, setSelectedGrowthStage] = useState( - null, - ); - const [activeTab, setActiveTab] = useState(0); - const [irrigationPlan, setIrrigationPlan] = useState(null); - const [fertilizerPlan, setFertilizerPlan] = useState(null); - const [completedTasks, setCompletedTasks] = useState([]); - const [waterBalance, setWaterBalance] = useState(null); - const [loading, setLoading] = useState(false); - const [requestError, setRequestError] = useState(null); - const [statusMessage, setStatusMessage] = useState(null); const primaryMain = theme.palette.primary.main; const primaryLight = theme.palette.primary.light; const primaryDark = theme.palette.primary.dark; const paperBg = theme.palette.background.paper; - const completedTaskCount = completedTasks.length; + + const [growthStages, setGrowthStages] = useState([]); + const [cropOptions, setCropOptions] = useState([]); + const [configLoading, setConfigLoading] = useState(true); + const [configError, setConfigError] = useState(null); + const [growthStage, setGrowthStage] = useState(""); + const [selectedCrop, setSelectedCrop] = useState(null); + const [recommendation, setRecommendation] = + useState(null); + const [loading, setLoading] = useState(false); + const [requestError, setRequestError] = useState(null); + const [statusMessage, setStatusMessage] = useState(null); + const [reasoningExpanded, setReasoningExpanded] = useState(false); + const [area, setArea] = useState(1); + const [historyItems, setHistoryItems] = useState([]); + const [historyPage, setHistoryPage] = useState(0); + const [historyPageSize, setHistoryPageSize] = useState(10); + const [historyPagination, setHistoryPagination] = useState({ + page: 1, + page_size: 10, + total_pages: 0, + total_items: 0, + has_next: false, + has_previous: false, + next: null, + previous: null, + }); + const [historyLoading, setHistoryLoading] = useState(false); + const [historyError, setHistoryError] = useState(null); + const [detailsSheet, setDetailsSheet] = useState({ + isOpen: false, + title: "", + content: "", + type: "section", + }); useEffect(() => { - setIrrigationPlan(null); - setFertilizerPlan(null); - setCompletedTasks([]); - setWaterBalance(null); + setRecommendation(null); setRequestError(null); setSelectedCrop(null); setGrowthStages([]); - setSelectedGrowthStage(null); - - if (!farmUuid) { - setConfigError(t("errors.noFarm")); - setConfigLoading(false); - return; - } - + setGrowthStage(""); setConfigLoading(true); setConfigError(null); - selectedPlantsService - .getSelected(farmUuid) - .then((plants) => { - const crops = plants.map((plant) => ({ - id: plant.name, - name: plant.name, - icon: getPlantIcon(plant.icon), - growthStages: plant.growth_stages, - })); - setCropOptions(crops); + const timer = window.setTimeout(() => { + setCropOptions(MOCK_CROP_OPTIONS); - const firstPlant = crops[0]; - if (firstPlant) { - setSelectedCrop(firstPlant.id); - setGrowthStages(firstPlant.growthStages ?? []); - setSelectedGrowthStage(firstPlant.growthStages?.[0] ?? null); - } - }) - .catch((err: { message?: string }) => { - setConfigError(err?.message ?? "Failed to load plants"); - }) - .finally(() => setConfigLoading(false)); - }, [farmUuid, t]); + const firstCrop = MOCK_CROP_OPTIONS[0]; + if (firstCrop) { + setSelectedCrop(firstCrop.id); + const stages = + firstCrop.growthStages?.map((stage) => ({ + id: stage, + icon: getGrowthStageIcon(stage), + label: formatStageLabel(stage), + })) ?? []; + + setGrowthStages(stages); + setGrowthStage(stages[0]?.id ?? ""); + } + + setConfigLoading(false); + }, 300); + + return () => window.clearTimeout(timer); + }, []); + + useEffect(() => { + setHistoryLoading(true); + setHistoryError(null); + + const timer = window.setTimeout(() => { + const start = historyPage * historyPageSize; + const end = start + historyPageSize; + const data = MOCK_HISTORY_SOURCE.slice(start, end); + const totalItems = MOCK_HISTORY_SOURCE.length; + const totalPages = Math.max(1, Math.ceil(totalItems / historyPageSize)); + + setHistoryItems(data); + setHistoryPagination({ + page: historyPage + 1, + page_size: historyPageSize, + total_pages: totalPages, + total_items: totalItems, + has_next: historyPage + 1 < totalPages, + has_previous: historyPage > 0, + next: historyPage + 1 < totalPages ? String(historyPage + 2) : null, + previous: historyPage > 0 ? String(historyPage) : null, + }); + setHistoryLoading(false); + }, 250); + + return () => window.clearTimeout(timer); + }, [historyPage, historyPageSize]); + + const handleGenerate = () => { + if (!selectedCrop || !growthStage) return; + + const crop = cropOptions.find((option) => option.id === selectedCrop); + const stage = growthStages.find((item) => item.id === growthStage); - const handleGenerate = async () => { - if (!selectedCrop || !farmUuid) return; setLoading(true); - setIrrigationPlan(null); - setFertilizerPlan(null); - setCompletedTasks([]); - setWaterBalance(null); + setArea(1); + setRecommendation(null); setRequestError(null); setStatusMessage(t("generating")); - try { - const recommendation = await irrigationRecommendationService.recommend({ - farm_uuid: farmUuid, - crop_id: selectedCrop, - growth_stage: selectedGrowthStage ?? undefined, - }); + setReasoningExpanded(false); - if ("task_id" in recommendation) { - let attempts = 0; - let taskStatus = - await irrigationRecommendationService.getRecommendStatus( - recommendation.task_id, - farmUuid, - ); - - while (isRecommendationTaskRunning(taskStatus.status)) { - attempts += 1; - setStatusMessage(taskStatus.progress?.message ?? t("generating")); - - if (attempts >= 20) { - throw new Error(t("errors.timeout")); - } - - await sleep(1500); - taskStatus = await irrigationRecommendationService.getRecommendStatus( - recommendation.task_id, - farmUuid, - ); - } - - if (taskStatus.status === "failed" || !taskStatus.result?.plan) { - throw new Error(taskStatus.error ?? t("errors.generateFailed")); - } - - setIrrigationPlan(taskStatus.result.plan); - setFertilizerPlan(MOCK_FERTILIZER_PLAN); - setWaterBalance(taskStatus.result.water_balance ?? null); - - return; - } - - if (!recommendation.plan) { - throw new Error(t("errors.generateFailed")); - } - - setIrrigationPlan(recommendation.plan); - setFertilizerPlan(MOCK_FERTILIZER_PLAN); - setWaterBalance(recommendation.water_balance ?? null); - } catch (error) { - setIrrigationPlan(null); - setFertilizerPlan(null); - setCompletedTasks([]); - setWaterBalance(null); - setRequestError(getErrorMessage(error, t("errors.generateFailed"))); - } finally { + window.setTimeout(() => { + setRecommendation( + buildMockRecommendation( + crop?.name ?? selectedCrop, + stage?.label ?? formatStageLabel(growthStage), + ), + ); setLoading(false); setStatusMessage(null); - } + }, 700); }; - const moistureLevelValue = - typeof irrigationPlan?.moistureLevel === "number" - ? irrigationPlan.moistureLevel - : Number(irrigationPlan?.moistureLevel); - const hasNumericMoistureLevel = Number.isFinite(moistureLevelValue); - const nextWaterBalanceDay = waterBalance?.daily?.[0]; - const toggleCompletedTask = (taskId: number) => { - setCompletedTasks((prev) => - prev.includes(taskId) - ? prev.filter((id) => id !== taskId) - : [...prev, taskId], - ); - }; + const stageIndex = growthStages.findIndex((stage) => stage.id === growthStage); + const selectedCropOption = + cropOptions.find((option) => option.id === selectedCrop) ?? null; + const selectedGrowthStage = + growthStages.find((stage) => stage.id === growthStage) ?? null; + const resultContext = `${selectedCropOption?.name ?? selectedCrop ?? ""} | ${ + selectedGrowthStage?.label ?? formatStageLabel(growthStage) + }`; + + const primaryMethod = recommendation?.primary_method ?? null; + const usageSummary = recommendation?.usage_summary ?? null; + const waterBalance = recommendation?.water_balance ?? null; + const waterBalanceDaily = waterBalance?.daily ?? []; + const alternativePlans = recommendation?.alternative_plans ?? []; + const applicationSteps = recommendation?.timeline ?? []; + const scheduleSection = + recommendation?.sections.find((section) => section.type === "schedule") ?? null; + const warningSections = + recommendation?.sections.filter((section) => section.type === "warning") ?? []; + const tipSections = recommendation?.sections.filter((section) => section.type === "tip") ?? []; + const totalWaterMm = (usageSummary?.area_basis_mm ?? 0) * area; + const totalWaterLiters = (usageSummary?.area_basis_liters ?? 0) * area; + + const warningMessages = useMemo(() => { + const items = [ + recommendation?.plan.warning, + ...warningSections.map((section) => section.content), + ].filter((item): item is string => Boolean(item)); + + return Array.from(new Set(items)); + }, [recommendation?.plan.warning, warningSections]); const handleCropSelect = (crop: CropOption) => { setSelectedCrop((prev) => { const nextCrop = prev === crop.id ? null : crop.id; const nextStages = - cropOptions.find((option) => option.id === nextCrop)?.growthStages ?? []; + nextCrop && crop.growthStages?.length + ? crop.growthStages.map((stage) => ({ + id: stage, + icon: getGrowthStageIcon(stage), + label: formatStageLabel(stage), + })) + : []; - setGrowthStages(nextCrop ? nextStages : []); - setSelectedGrowthStage(nextCrop ? nextStages[0] ?? null : null); + setGrowthStages(nextStages); + setGrowthStage(nextStages[0]?.id ?? ""); + setRecommendation(null); return nextCrop; }); }; + const handleBackToForm = () => { + setRecommendation(null); + setArea(1); + setReasoningExpanded(false); + }; + + const handleAreaInputChange = (value: string) => { + if (value === "") { + setArea(0.5); + return; + } + + const nextValue = Number(value); + if (Number.isNaN(nextValue)) return; + + setArea(Math.min(100, Math.max(0.5, nextValue))); + }; + + const openWaterBalanceDetails = (item: WaterBalanceDailyEntry) => { + setDetailsSheet({ + isOpen: true, + title: `جزئیات تراز آب ${item.forecast_date}`, + content: `ET0: ${formatNumber(item.et0_mm)} میلی متر\nETc: ${formatNumber( + item.etc_mm, + )} میلی متر\nبارش موثر: ${formatNumber( + item.effective_rainfall_mm, + )} میلی متر\nآبیاری ناخالص: ${formatNumber( + item.gross_irrigation_mm, + )} میلی متر\nبازه اجرا: ${item.irrigation_timing}`, + type: "balance", + }); + }; + + const openAlternativeDetails = (item: AlternativePlan) => { + setDetailsSheet({ + isOpen: true, + title: item.title, + content: item.description, + type: "alternative", + }); + }; + + const openSectionDetails = (item: RecommendationSection) => { + setDetailsSheet({ + isOpen: true, + title: item.title, + content: item.content, + type: "section", + }); + }; + + const closeDetailsSheet = () => { + setDetailsSheet((prev) => ({ ...prev, isOpen: false })); + }; + + const handleViewRecommendationReport = (recommendationUuid: string) => { + const historyItem = MOCK_HISTORY_SOURCE.find( + (item) => item.recommendation_uuid === recommendationUuid, + ); + + if (!historyItem) { + setRequestError("گزارش مورد نظر در داده های ماک پیدا نشد."); + return; + } + + setLoading(true); + setRequestError(null); + setStatusMessage("در حال دریافت گزارش توصیه"); + + window.setTimeout(() => { + setRecommendation(historyItem.result); + setReasoningExpanded(false); + setArea(1); + setLoading(false); + setStatusMessage(null); + }, 400); + }; + return ( - `linear-gradient(165deg, ${alpha(theme.palette.primary.main, 0.08)} 0%, ${alpha(theme.palette.primary.main, 0.04)} 35%, ${alpha(theme.palette.primary.main, 0.02)} 70%, ${theme.palette.background.default} 100%)`, + background: (th) => + `linear-gradient(165deg, ${alpha(th.palette.primary.main, 0.08)} 0%, ${alpha(th.palette.primary.main, 0.05)} 25%, ${alpha(th.palette.primary.main, 0.03)} 60%, ${th.palette.background.default} 100%)`, minHeight: "100vh", }} > - - {/* 1) Dynamic Header */} - - - {t("title")} - - - {t("subtitle")} - - - - {/* 2) Growth Stage Selector */} - {!!growthStages.length && ( - <> - + {recommendation ? ( + + - {t("growthStage.title")} - - - {growthStages.map((stage) => { - const isSelected = selectedGrowthStage === stage; + + + + + + {resultContext} + + + - return ( - setSelectedGrowthStage(stage)} - className="flex flex-col items-center gap-1.5 px-4 py-3 rounded-2xl shrink-0 transition-all duration-300 ease-out border-2 cursor-pointer min-w-[84px]" + + + + - - - - - {formatStageLabel(stage)} + + + + + {recommendation.recommendation_title} + + + {primaryMethod?.label} + + + {recommendation.recommendation_subtitle} + + + + + + + + + + + رطوبت هدف: %{formatNumber(primaryMethod?.target_moisture ?? 0)} + + + + + + + محاسبه آب مورد نیاز برای مساحت مزرعه + + + هکتار + + + + + setArea(value as number)} + valueLabelDisplay="auto" + sx={{ + color: primaryMain, + "& .MuiSlider-valueLabel": { + backgroundColor: primaryDark, + borderRadius: "10px", + }, + }} + /> + handleAreaInputChange(event.target.value)} + inputProps={{ min: 0.5, max: 100, step: 0.5 }} + label="مساحت" + className="rounded-2xl" + sx={{ + "& .MuiOutlinedInput-root": { + borderRadius: "18px", + backgroundColor: alpha(theme.palette.background.paper, 0.88), + }, + }} + /> + + + + + نیاز کل مزرعه شما: + + + {formatNumber(totalWaterMm)} میلی متر + + + معادل {formatNumber(totalWaterLiters)} لیتر آب برای کل سطح انتخابی + + + مبنا: {formatNumber(usageSummary?.area_basis_mm ?? 0)} میلی متر در هر هکتار + + + + + + + + + + + تراز آب و نیاز روزانه + + + {waterBalanceDaily.map((item) => ( + openWaterBalanceDetails(item)} + sx={{ + "&:hover": { + backgroundColor: alpha(primaryMain, 0.04), + }, + }} + > + + + {item.forecast_date} + + + {formatNumber(item.gross_irrigation_mm)} mm + + + + + بازه اجرا: {item.irrigation_timing} + + + ))} + + + + + پروفایل گیاه + + + + + + + + - ); - })} + + + + + + + + + + + {t("result.title")} + + + + + + + + + + + {scheduleSection?.content && ( + + + + )} + + + setReasoningExpanded(!reasoningExpanded)} + className="w-full flex items-center justify-between px-4 py-3 text-start cursor-pointer" + sx={{ "&:hover": { bgcolor: alpha(primaryMain, 0.06) } }} + > + + + + چرا این برنامه آبیاری پیشنهاد شده است؟ + + + + + + + + {scheduleSection?.content} + + + {primaryMethod?.coverage} + + + + + + + + + + + + مراحل و دستورالعمل اجرا + + + + {applicationSteps.map((item, index) => ( + + {item.title} + + + {item.description} + + + + ))} + + + + + + + {warningMessages.length > 0 && ( + + + هشدارها و نکات مهم + + + {warningMessages.map((warning, index) => ( + + {warning} + + ))} + + + )} + + + + برنامه های جایگزین + + + اگر شرایط اقلیمی یا برنامه بهره برداری تغییر کرد، می توانید از نسخه های جایگزین زیر استفاده کنید. + + + + {alternativePlans.map((item) => ( + openAlternativeDetails(item)} + sx={{ + border: `1px solid ${alpha(primaryMain, 0.12)}`, + background: `linear-gradient(180deg, ${alpha(theme.palette.background.paper, 0.98)} 0%, ${alpha(primaryMain, 0.04)} 100%)`, + boxShadow: `0 6px 18px ${alpha(primaryMain, 0.08)}`, + cursor: "pointer", + }} + > + + + + + + + + + + {item.title} + + + + {item.description} + + + + + + ))} + + + {!!tipSections.length && ( + + {tipSections.map((item) => ( + openSectionDetails(item)} + sx={{ + borderRadius: "20px", + border: `1px solid ${alpha(primaryMain, 0.12)}`, + background: `linear-gradient(180deg, ${paperBg} 0%, ${alpha(primaryMain, 0.04)} 100%)`, + cursor: "pointer", + }} + > + + + + + + + {item.title} + + + {item.content} + + + + + ))} + + )} + + + + + + + + + + + ) : ( + <> + + + {t("title")} + + + {t("subtitle")} + + + + {!!growthStages.length && ( + <> + + {t("growthStage.title")} + + + {growthStages.map((stage, idx) => { + const isSelected = growthStage === stage.id; + const isPast = idx < stageIndex; + + return ( + setGrowthStage(stage.id)} + className="flex flex-col items-center gap-1.5 px-4 py-3 rounded-2xl shrink-0 transition-all duration-300 ease-out border-2 cursor-pointer min-w-[72px]" + sx={{ + borderColor: isSelected ? primaryMain : "transparent", + background: isSelected + ? `linear-gradient(145deg, ${alpha(primaryMain, 0.15)} 0%, ${alpha(primaryMain, 0.06)} 100%)` + : `linear-gradient(145deg, ${paperBg} 0%, ${alpha(primaryMain, 0.04)} 100%)`, + boxShadow: isSelected + ? `0 4px 20px ${alpha(primaryMain, 0.2)}, inset 0 1px 0 rgba(255,255,255,0.8)` + : "0 2px 8px rgba(0,0,0,0.04)", + "&:hover": { + transform: "translateY(-2px)", + boxShadow: isSelected + ? `0 6px 24px ${alpha(primaryMain, 0.25)}` + : `0 4px 16px ${alpha(primaryMain, 0.1)}`, + }, + }} + > + + + + + {stage.label ?? formatStageLabel(stage.id)} + + + ); + })} + + + )} + + + {t("plantSelection.title")} + + {configLoading ? ( + + + + ) : configError ? ( + + {configError} + + ) : ( + + {cropOptions.map((crop) => ( + handleCropSelect(crop)} + /> + ))} + + )} + + + + + + {requestError && !loading && ( + + {requestError} + + )} + + + {loading ? ( + + + + + + + {statusMessage ?? "در حال تحلیل و تولید نسخه آبیاری..."} + + + + ) : ( + <> + + + + تاریخچه توصیه های آبیاری + + + همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید. + + + + + + + {historyError && ( + + {historyError} + + )} + + + + + + تاریخ ثبت + محصول / مرحله رشد + روش آبیاری + وضعیت + گزارش + + + + {historyLoading ? ( + + + + + + ) : historyItems.length ? ( + historyItems.map((item) => ( + + {formatDateTime(item.requested_at)} + + + + {item.plant_name} + + + {formatStageLabel(item.growth_stage)} + + + + {item.irrigation_method} + + + + + + + handleViewRecommendationReport( + item.recommendation_uuid, + ) + } + sx={{ + border: `1px solid ${alpha(primaryMain, 0.16)}`, + borderRadius: "12px", + }} + > + + + + + + )) + ) : ( + + + + هنوز توصیه ای برای این مزرعه ثبت نشده است. + + + + )} + +
+
+ + setHistoryPage(nextPage)} + rowsPerPage={historyPageSize} + onRowsPerPageChange={(event) => { + setHistoryPage(0); + setHistoryPageSize(Number(event.target.value)); + }} + rowsPerPageOptions={[5, 10, 20]} + /> +
+
+ + )}
)} - {/* 3) Plant Selection Section */} - - {t("plantSelection.title")} - - {configLoading ? ( - - - - ) : configError ? ( - - {configError} - - ) : ( - - {(cropOptions.length > 0 ? cropOptions : []).map((crop) => ( - handleCropSelect(crop)} - /> - ))} - - )} - - {/* 4) Primary CTA Button - End of form */} - - - - - {requestError && !loading && ( - - {requestError} - - )} - - setActiveTab(value)} - indicatorColor="primary" - textColor="primary" - variant="fullWidth" - className="mb-6" - sx={{ - bgcolor: alpha(primaryMain, 0.04), - borderRadius: "18px", - minHeight: 56, - "& .MuiTabs-indicator": { - height: 3, - borderRadius: 999, - }, - }} - > - - - - - {/* 5) Result Card (after click) */} - {activeTab === 0 && irrigationPlan && ( - - - - {/* Circular moisture indicator */} - - {hasNumericMoistureLevel ? ( - - - - - - - - - - - - - - - {moistureLevelValue}% - - - {t("result.moistureLevel")} - - - - ) : ( - - - - {String(irrigationPlan.moistureLevel)} - - - {t("result.moistureLevel")} - - - )} - - - - - - - - - {waterBalance && ( - - - {t("result.waterBalance")} - - {nextWaterBalanceDay && ( - <> - - - - - )} - {typeof waterBalance.active_kc === "number" && ( - - )} - - )} - - {irrigationPlan.warning && ( - - - - - - {t("result.smartWarning")} - - - {irrigationPlan.warning} - - - - - )} - - - - )} - - {activeTab === 1 && fertilizerPlan && ( - - - - - - Visual Summary - - - {fertilizerPlan.status} for {fertilizerPlan.crop} - - - - - {fertilizerPlan.alerts.map((alert) => ( - - {alert.title} - {alert.message} - - ))} - - - - - - Nutrient Levels - - - - - - - - - - - - - - - - - - - - - Fertilization Schedule - - - {fertilizerPlan.stages.map((stage, index) => ( - - - - {index < fertilizerPlan.stages.length - 1 && ( - - )} - - - - - {stage.phase} - - - {stage.time} - - - {stage.summary} - - - - - ))} - - - - - - - - Current Stage Recommendations - - - - {fertilizerPlan.recommendedFertilizers.map((item) => { - const isWaterBased = - item.method.toLowerCase().includes("fertigation") || - item.method.toLowerCase().includes("foliar"); - - return ( - - - - - {isWaterBased ? ( - - ) : ( - - )} - - - - - - {item.name} - - - Type: {item.type} - - - - - - - Dosage - - - {item.dosage} - - - - - Method - - - {item.method} - - - - - - - - ); - })} - - - - - Action List - - - {fertilizerPlan.recommendedFertilizers.map((item) => { - const isCompleted = completedTasks.includes(item.id); - - return ( - toggleCompletedTask(item.id)} - sx={{ color: primaryMain }} - /> - } - className="m-0 items-start rounded-xl px-2 py-1" - label={ - - {`Apply ${item.dosage} of ${item.name}`} - - } - /> - ); - })} - - - - - - Next action required in 14 days. {completedTaskCount} of{" "} - {fertilizerPlan.recommendedFertilizers.length} tasks completed. - - - - - - - - - - - - - )} - - {/* Loading state */} - {loading && ( + {loading && recommendation && ( - + + + {statusMessage ?? t("generating")} @@ -1123,12 +1544,57 @@ export default function SmartIrrigationRecommendation() { )}
+ + + + + + {detailsSheet.title} + + + + + + + {detailsSheet.content} + + + + +
); } -// ─── Sub-components ────────────────────────────────────────────────────────── - function CropCard({ crop, label, @@ -1144,6 +1610,7 @@ function CropCard({ const primaryMain = theme.palette.primary.main; const primaryDark = theme.palette.primary.dark; const paperBg = theme.palette.background.paper; + return ( - + {label} {selected && ( - + )} ); } -function ResultRow({ +function PrescriptionRow({ icon, label, value, @@ -1208,15 +1668,16 @@ function ResultRow({ }) { const theme = useTheme(); const primaryMain = theme.palette.primary.main; + return ( - + {label}