diff --git a/src/libs/api/services/fertilizationRecommendationService.ts b/src/libs/api/services/fertilizationRecommendationService.ts index ffe086d..bf3d32f 100644 --- a/src/libs/api/services/fertilizationRecommendationService.ts +++ b/src/libs/api/services/fertilizationRecommendationService.ts @@ -142,6 +142,31 @@ interface ApiResponse { data: T; } +export interface FertilizationRecommendationHistoryItem { + recommendation_uuid: string; + plant_name: string; + growth_stage: string; + fertilizer_type: string; + status: "pending_confirmation" | "in_progress" | "completed" | string; + status_label: string; + requested_at: string; +} + +export interface FertilizationRecommendationHistoryPagination { + 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: FertilizationRecommendationHistoryPagination; +} + async function unwrap(promise: Promise>): Promise { const res = await promise; return res.data; @@ -166,4 +191,34 @@ export const fertilizationRecommendationService = { ), ); }, + + async getRecommendationsHistory( + farmUuid: string, + page = 1, + pageSize = 10, + ): Promise<{ + data: FertilizationRecommendationHistoryItem[]; + pagination: FertilizationRecommendationHistoryPagination; + }> { + const response = await apiClient.get< + PaginatedApiResponse + >( + `${RECOMMEND_PREFIX}/recommendations/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`, + ); + + return { + data: response.data, + pagination: response.pagination, + }; + }, + + getRecommendationDetail( + recommendationUuid: string, + ): Promise { + return unwrap( + apiClient.get>( + `${RECOMMEND_PREFIX}/recommendations/${encodeURIComponent(recommendationUuid)}/`, + ), + ); + }, }; diff --git a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx index 2726c01..42f56c3 100644 --- a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx +++ b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx @@ -15,6 +15,14 @@ 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"; @@ -28,6 +36,8 @@ import { type CropOption, type FertilizationAlternativeRecommendation, type FertilizationNutrientItem, + type FertilizationRecommendationHistoryItem, + type FertilizationRecommendationHistoryPagination, type FertilizationRecommendationResult, type GrowthStage, } from "@/libs/api/services/fertilizationRecommendationService"; @@ -93,6 +103,29 @@ const formatUnitLabel = (unit: string) => { return unit; }; +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 getStatusChipColor = ( + status: string, +): "warning" | "info" | "default" => { + if (status === "in_progress") return "info"; + if (status === "completed") return "default"; + + return "warning"; +}; + export default function SmartFertilizationRecommendation() { const t = useTranslations("fertilization"); const theme = useTheme(); @@ -116,6 +149,24 @@ export default function SmartFertilizationRecommendation() { const [statusMessage, setStatusMessage] = useState(null); const [reasoningExpanded, setReasoningExpanded] = useState(false); const [area, setArea] = useState(1); + const [historyItems, setHistoryItems] = useState< + FertilizationRecommendationHistoryItem[] + >([]); + 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: "", @@ -170,6 +221,33 @@ export default function SmartFertilizationRecommendation() { .finally(() => setConfigLoading(false)); }, [farmUuid, t]); + useEffect(() => { + if (!farmUuid) { + setHistoryItems([]); + setHistoryError(null); + return; + } + + setHistoryLoading(true); + setHistoryError(null); + fertilizationRecommendationService + .getRecommendationsHistory( + farmUuid, + historyPage + 1, + historyPageSize, + ) + .then((response) => { + setHistoryItems(response.data); + setHistoryPagination(response.pagination); + }) + .catch((error) => { + setHistoryError( + getErrorMessage(error, "خطا در دریافت تاریخچه توصیه های کودهی"), + ); + }) + .finally(() => setHistoryLoading(false)); + }, [farmUuid, historyPage, historyPageSize]); + const handleGenerate = async () => { if (!selectedCrop || !growthStage || !farmUuid) return; @@ -295,6 +373,30 @@ export default function SmartFertilizationRecommendation() { setDetailsSheet((prev) => ({ ...prev, isOpen: false })); }; + const handleViewRecommendationReport = async (recommendationUuid: string) => { + setLoading(true); + setRequestError(null); + setStatusMessage("در حال دریافت گزارش توصیه"); + + try { + const response = + await fertilizationRecommendationService.getRecommendationDetail( + recommendationUuid, + ); + + setRecommendation(response); + setReasoningExpanded(false); + setArea(1); + } catch (error) { + setRequestError( + getErrorMessage(error, "خطا در دریافت گزارش توصیه کودهی"), + ); + } finally { + setLoading(false); + setStatusMessage(null); + } + }; + return ( )} + + + {loading ? ( + + + + + + + {statusMessage ?? "در حال تحلیل و تولید نسخه تغذیه ای..."} + + + + ) : ( + <> + + + + تاریخچه توصیه های کودهی + + + همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید. + + + + + + + {historyError && ( + + {historyError} + + )} + + + + + + تاریخ ثبت + محصول / مرحله رشد + نوع کود + وضعیت + گزارش + + + + {historyLoading ? ( + + + + + + ) : historyItems.length ? ( + historyItems.map((item) => ( + + {formatDateTime(item.requested_at)} + + + + {item.plant_name} + + + {formatStageLabel(item.growth_stage)} + + + + {item.fertilizer_type} + + + + + + + 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]} + /> +
+
+ + )} +
)} - {loading && ( + {loading && recommendation && ( ? error.message : fallback; +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 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, + }, + ], +}; + export default function SmartIrrigationRecommendation() { const t = useTranslations("irrigation"); const theme = useTheme(); @@ -78,7 +207,10 @@ export default function SmartIrrigationRecommendation() { const [selectedGrowthStage, setSelectedGrowthStage] = useState( null, ); - const [plan, setPlan] = 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); @@ -87,9 +219,12 @@ export default function SmartIrrigationRecommendation() { const primaryLight = theme.palette.primary.light; const primaryDark = theme.palette.primary.dark; const paperBg = theme.palette.background.paper; + const completedTaskCount = completedTasks.length; useEffect(() => { - setPlan(null); + setIrrigationPlan(null); + setFertilizerPlan(null); + setCompletedTasks([]); setWaterBalance(null); setRequestError(null); setSelectedCrop(null); @@ -132,7 +267,9 @@ export default function SmartIrrigationRecommendation() { const handleGenerate = async () => { if (!selectedCrop || !farmUuid) return; setLoading(true); - setPlan(null); + setIrrigationPlan(null); + setFertilizerPlan(null); + setCompletedTasks([]); setWaterBalance(null); setRequestError(null); setStatusMessage(t("generating")); @@ -170,7 +307,8 @@ export default function SmartIrrigationRecommendation() { throw new Error(taskStatus.error ?? t("errors.generateFailed")); } - setPlan(taskStatus.result.plan); + setIrrigationPlan(taskStatus.result.plan); + setFertilizerPlan(MOCK_FERTILIZER_PLAN); setWaterBalance(taskStatus.result.water_balance ?? null); return; @@ -180,10 +318,13 @@ export default function SmartIrrigationRecommendation() { throw new Error(t("errors.generateFailed")); } - setPlan(recommendation.plan); + setIrrigationPlan(recommendation.plan); + setFertilizerPlan(MOCK_FERTILIZER_PLAN); setWaterBalance(recommendation.water_balance ?? null); } catch (error) { - setPlan(null); + setIrrigationPlan(null); + setFertilizerPlan(null); + setCompletedTasks([]); setWaterBalance(null); setRequestError(getErrorMessage(error, t("errors.generateFailed"))); } finally { @@ -193,11 +334,18 @@ export default function SmartIrrigationRecommendation() { }; const moistureLevelValue = - typeof plan?.moistureLevel === "number" - ? plan.moistureLevel - : Number(plan?.moistureLevel); + 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 handleCropSelect = (crop: CropOption) => { setSelectedCrop((prev) => { @@ -380,8 +528,35 @@ export default function SmartIrrigationRecommendation() { )} + 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) */} - {plan && ( + {activeTab === 0 && irrigationPlan && ( - {String(plan.moistureLevel)} + {String(irrigationPlan.moistureLevel)} {t("result.moistureLevel")} @@ -483,17 +658,17 @@ export default function SmartIrrigationRecommendation() { @@ -535,7 +710,7 @@ export default function SmartIrrigationRecommendation() {
)} - {plan.warning && ( + {irrigationPlan.warning && ( - {plan.warning} + {irrigationPlan.warning} @@ -567,6 +742,366 @@ export default function SmartIrrigationRecommendation() { )} + {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 && (