diff --git a/FERTILIZATION_RECOMMENDATION_RESULT_SPEC.md b/FERTILIZATION_RECOMMENDATION_RESULT_SPEC.md new file mode 100644 index 0000000..fff3380 --- /dev/null +++ b/FERTILIZATION_RECOMMENDATION_RESULT_SPEC.md @@ -0,0 +1,487 @@ +# Fertilization Recommendation Result API Spec + +این فایل مشخص می‌کند که فرانت‌اند برای صفحه `SmartFertilizationRecommendation` دقیقاً چه خروجی‌ای از بک‌اند نیاز دارد، مخصوصاً برای: + +- Hero Card +- ماشین‌حساب مساحت مزرعه +- آنالیز ترکیبات +- مراحل مصرف +- نکات ایمنی +- کودهای جایگزین +- Bottom Sheet جزئیات + +نکته مهم: برای ماشین‌حساب، فرانت‌اند **نباید** مقدار را از رشته‌هایی مثل `150 kg/ha` parse کند. بک‌اند باید مقادیر عددی استاندارد و مستقل برگرداند. + +--- + +## هدف اصلی + +برای اینکه ماشین‌حساب مقدار مصرف دقیق کار کند، بک‌اند باید علاوه بر متن نمایشی، **مقدار عددی پایه** را نیز برگرداند. + +فرمول مورد نیاز فرانت: + +```text +مقدار کل = مقدار مصرف در هر متر مربع × مساحت مزرعه +``` + +یا اگر واحد پایه بر حسب هکتار ارسال شود: + +```text +مقدار کل = مقدار مصرف در هکتار × مساحت (هکتار) +``` + +اما پیشنهاد قطعی برای فرانت این است که بک‌اند هر دو را بدهد: + +- `base_amount_per_hectare` +- `base_amount_per_square_meter` + +تا هیچ تبدیل واحدی در UI لازم نباشد. + +--- + +## Endpoint پیشنهادی + +```text +POST /api/fertilization/recommend/ +``` + +یا اگر ساختار فعلی پروژه حفظ شود: + +```text +POST /api/fertilization-recommendation/recommend/ +``` + +--- + +## Request پیشنهادی + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "crop_id": "wheat", + "growth_stage": "flowering", + "area": { + "value": 2.5, + "unit": "hectare" + } +} +``` + +### توضیح فیلدهای Request + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `farm_uuid` | string | بله | شناسه مزرعه | +| `crop_id` | string | بله | شناسه محصول | +| `growth_stage` | string | بله | مرحله رشد محصول | +| `area.value` | number | اختیاری | مساحت مزرعه برای محاسبه مستقیم مقدار کل | +| `area.unit` | string | اختیاری | واحد مساحت؛ ترجیحاً `hectare` یا `square_meter` | + +اگر `area` ارسال نشود، فرانت با داده‌های پایه محاسبه را خودش انجام می‌دهد. + +--- + +## Response پیشنهادی + +```json +{ + "status": "success", + "data": { + "recommendation_id": "fert-rec-001", + "crop": { + "id": "wheat", + "name": "گندم" + }, + "growth_stage": { + "id": "flowering", + "name": "گلدهی" + }, + "primary_recommendation": { + "fertilizer_code": "npk-20-20-20", + "fertilizer_name": "کود کامل 20-20-20", + "display_title": "کود کامل 20-20-20", + "fertilizer_type": "NPK", + "npk_ratio": { + "n": 20, + "p": 20, + "k": 20, + "label": "20-20-20" + }, + "application_method": { + "id": "foliar_fertigation", + "label": "محلول پاشی / آب آبیاری" + }, + "application_interval": { + "value": 14, + "unit": "day", + "label": "هر 14 روز" + }, + "dosage": { + "base_amount_per_hectare": 150, + "base_amount_per_square_meter": 0.015, + "unit": "kg", + "label": "150 کیلوگرم در هکتار", + "calculation_basis": "product" + }, + "total_amount": { + "value": 375, + "unit": "kg", + "label": "375 کیلوگرم" + }, + "reasoning": "این ترکیب برای مرحله گلدهی به دلیل نیاز متعادل به ازت، فسفر و پتاسیم پیشنهاد شده است.", + "summary": "مناسب برای حفظ رشد رویشی و پشتیبانی از گلدهی" + }, + "nutrient_analysis": { + "macro": [ + { + "key": "n", + "name": "نیتروژن (N)", + "value": 20, + "unit": "percent", + "description": "نیتروژن برای رشد رویشی و افزایش سطح برگ ضروری است." + }, + { + "key": "p", + "name": "فسفر (P)", + "value": 20, + "unit": "percent", + "description": "فسفر برای توسعه ریشه و انتقال انرژی اهمیت دارد." + }, + { + "key": "k", + "name": "پتاسیم (K)", + "value": 20, + "unit": "percent", + "description": "پتاسیم به کیفیت محصول و مقاومت به تنش کمک می‌کند." + } + ], + "micro": [ + { + "key": "fe", + "name": "آهن", + "value": 0.5, + "unit": "percent", + "description": "آهن در تولید کلروفیل و جلوگیری از زردی موثر است." + }, + { + "key": "zn", + "name": "روی", + "value": 1, + "unit": "percent", + "description": "روی در رشد متعادل و فعالیت آنزیم‌ها نقش دارد." + } + ] + }, + "application_guide": { + "safety_warning": "هنگام محلول پاشی از دستکش و ماسک استفاده کنید و در ساعات خنک روز مصرف انجام شود.", + "steps": [ + { + "step_number": 1, + "title": "آماده سازی", + "description": "مقدار توصیه شده از کود را در یک سطل آب تمیز حل کنید." + }, + { + "step_number": 2, + "title": "ترکیب", + "description": "محلول را به مخزن اصلی سم پاش یا سیستم آبیاری اضافه کنید و خوب هم بزنید." + }, + { + "step_number": 3, + "title": "مصرف", + "description": "به صورت یکنواخت روی گیاه اسپری کنید یا در سیستم آبیاری تزریق نمایید." + } + ] + }, + "alternative_recommendations": [ + { + "fertilizer_code": "npk-10-52-10", + "fertilizer_name": "کود 10-52-10", + "fertilizer_type": "NPK (فسفر بالا)", + "usage_method": "محلول پاشی", + "description": "برای تقویت ریشه و پشتیبانی از گلدهی در صورت نبود پیشنهاد اصلی مناسب است." + }, + { + "fertilizer_code": "npk-12-12-36", + "fertilizer_name": "کود 12-12-36", + "fertilizer_type": "NPK (پتاس بالا)", + "usage_method": "تزریق در آبیاری", + "description": "برای بهبود کیفیت محصول و افزایش پتاسیم قابل استفاده است." + } + ] + } +} +``` + +--- + +## فیلدهای ضروری برای فرانت + +### 1) اطلاعات اصلی پیشنهاد + +این فیلدها برای Hero Card لازم‌اند. + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `primary_recommendation.fertilizer_name` | string | بله | نام اصلی کود | +| `primary_recommendation.display_title` | string | بهتر است | عنوان نمایشی اگر با نام اصلی فرق دارد | +| `primary_recommendation.fertilizer_type` | string | بله | نوع کود مثل NPK | +| `primary_recommendation.npk_ratio.label` | string | بله | متن آماده برای نمایش مثل `20-20-20` | +| `primary_recommendation.npk_ratio.n` | number | بله | درصد نیتروژن | +| `primary_recommendation.npk_ratio.p` | number | بله | درصد فسفر | +| `primary_recommendation.npk_ratio.k` | number | بله | درصد پتاسیم | +| `primary_recommendation.application_method.label` | string | بله | روش مصرف نمایشی | +| `primary_recommendation.application_interval.label` | string | بله | فاصله مصرف نمایشی | +| `primary_recommendation.reasoning` | string | بله | دلیل توصیه برای بخش توضیحات | + +### 2) فیلدهای ضروری برای ماشین‌حساب + +این بخش مهم‌ترین قسمت برای بک‌اند است. + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `primary_recommendation.dosage.base_amount_per_hectare` | number | بله | مقدار پایه مصرف در هر هکتار | +| `primary_recommendation.dosage.base_amount_per_square_meter` | number | بله | مقدار پایه مصرف در هر متر مربع | +| `primary_recommendation.dosage.unit` | string | بله | واحد مقدار مصرف؛ مثل `kg` یا `liter` | +| `primary_recommendation.dosage.label` | string | بله | متن آماده برای نمایش مثل `150 کیلوگرم در هکتار` | +| `primary_recommendation.total_amount.value` | number | اختیاری | اگر بک‌اند بر اساس مساحت ورودی مقدار کل را حساب کند | +| `primary_recommendation.total_amount.unit` | string | اختیاری | واحد مقدار کل | +| `primary_recommendation.total_amount.label` | string | اختیاری | متن نمایشی مقدار کل | + +### چرا `base_amount_per_square_meter` لازم است؟ + +چون شما گفتید ماشین‌حساب باید مقدار **کیلوگرم در هر متر مربع** را از بک‌اند بگیرد. + +پس بک‌اند باید این مقدار را صریح برگرداند، نه اینکه فرانت از `kg/ha` خودش تبدیل کند. + +نمونه: + +```json +{ + "base_amount_per_hectare": 150, + "base_amount_per_square_meter": 0.015, + "unit": "kg" +} +``` + +تبدیل مرجع: + +```text +1 hectare = 10,000 square meters +150 kg/ha = 0.015 kg/m² +``` + +--- + +## فیلدهای آنالیز ترکیبات + +برای اینکه فرانت مجبور به parse کردن متن `npkRatio` نباشد، بک‌اند باید آنالیز را ساختارمند بفرستد. + +### ماکرو + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `nutrient_analysis.macro[].key` | string | بله | کلید استاندارد مثل `n`, `p`, `k` | +| `nutrient_analysis.macro[].name` | string | بله | نام نمایشی فارسی | +| `nutrient_analysis.macro[].value` | number | بله | درصد عنصر | +| `nutrient_analysis.macro[].unit` | string | بله | معمولاً `percent` | +| `nutrient_analysis.macro[].description` | string | بهتر است | توضیح برای Bottom Sheet | + +### ریزمغذی‌ها + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `nutrient_analysis.micro[].key` | string | بله | مثل `fe`, `zn`, `mn`, `b` | +| `nutrient_analysis.micro[].name` | string | بله | نام فارسی عنصر | +| `nutrient_analysis.micro[].value` | number | بله | درصد عنصر | +| `nutrient_analysis.micro[].unit` | string | بله | معمولاً `percent` | +| `nutrient_analysis.micro[].description` | string | بهتر است | توضیح برای Bottom Sheet | + +--- + +## فیلدهای دستورالعمل مصرف + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `application_guide.safety_warning` | string | بله | متن هشدار ایمنی | +| `application_guide.steps[].step_number` | number | بله | شماره مرحله | +| `application_guide.steps[].title` | string | بله | عنوان مرحله | +| `application_guide.steps[].description` | string | بله | توضیح مرحله | + +--- + +## فیلدهای کودهای جایگزین + +| فیلد | نوع | اجباری | توضیح | +|------|-----|--------|-------| +| `alternative_recommendations[].fertilizer_code` | string | بله | شناسه یکتا | +| `alternative_recommendations[].fertilizer_name` | string | بله | نام کود جایگزین | +| `alternative_recommendations[].fertilizer_type` | string | بله | نوع کود | +| `alternative_recommendations[].usage_method` | string | بله | روش مصرف | +| `alternative_recommendations[].description` | string | بله | توضیح برای Bottom Sheet | + +--- + +## فیلدهای لازم برای Bottom Sheet + +برای باز شدن Bottom Sheet روی مواد غذایی و کودهای جایگزین، بهتر است توضیح آماده از بک‌اند بیاید. + +| بخش | فیلد پیشنهادی | +|-----|---------------| +| مواد اصلی | `nutrient_analysis.macro[].description` | +| ریزمغذی‌ها | `nutrient_analysis.micro[].description` | +| کود جایگزین | `alternative_recommendations[].description` | + +این باعث می‌شود فرانت مجبور نباشد متن‌های توضیحی hard-code کند. + +--- + +## فرمت واحدها + +پیشنهاد می‌شود بک‌اند از مقادیر استاندارد زیر استفاده کند: + +### واحد مقدار کود + +- `kg` +- `gram` +- `liter` +- `milliliter` + +### واحد مساحت + +- `hectare` +- `square_meter` + +### واحد درصد عناصر + +- `percent` + +### واحد فاصله مصرف + +- `day` +- `week` + +--- + +## قوانین پیشنهادی برای بک‌اند + +### 1) متن نمایشی و مقدار عددی را با هم برگردانید + +اشتباه: + +```json +{ + "amountPerHectare": "150 kg/ha" +} +``` + +صحیح: + +```json +{ + "dosage": { + "base_amount_per_hectare": 150, + "base_amount_per_square_meter": 0.015, + "unit": "kg", + "label": "150 کیلوگرم در هکتار" + } +} +``` + +### 2) هیچ داده مهمی فقط داخل `reasoning` دفن نشود + +مواردی مثل: + +- درصد N, P, K +- درصد ریزمغذی‌ها +- مقدار مصرف پایه +- روش مصرف + +باید فیلد مستقل داشته باشند، نه فقط متن آزاد. + +### 3) نام مرحله رشد و نام محصول را هم برگردانید + +تا Header صفحه بدون lookup اضافه ساخته شود. + +--- + +## حداقل خروجی لازم برای نسخه فعلی فرانت + +اگر بک‌اند بخواهد فقط حداقل داده‌ی لازم را برگرداند، این ساختار minimum پیشنهاد می‌شود: + +```json +{ + "status": "success", + "data": { + "crop": { + "id": "wheat", + "name": "گندم" + }, + "growth_stage": { + "id": "flowering", + "name": "گلدهی" + }, + "primary_recommendation": { + "fertilizer_name": "کود کامل 20-20-20", + "fertilizer_type": "NPK", + "npk_ratio": { + "n": 20, + "p": 20, + "k": 20, + "label": "20-20-20" + }, + "application_method": { + "label": "محلول پاشی / آب آبیاری" + }, + "application_interval": { + "label": "هر 14 روز" + }, + "dosage": { + "base_amount_per_hectare": 150, + "base_amount_per_square_meter": 0.015, + "unit": "kg", + "label": "150 کیلوگرم در هکتار" + }, + "reasoning": "توضیحات توصیه" + }, + "nutrient_analysis": { + "macro": [ + { "key": "n", "name": "نیتروژن (N)", "value": 20, "unit": "percent" }, + { "key": "p", "name": "فسفر (P)", "value": 20, "unit": "percent" }, + { "key": "k", "name": "پتاسیم (K)", "value": 20, "unit": "percent" } + ], + "micro": [] + }, + "application_guide": { + "safety_warning": "هشدار ایمنی", + "steps": [ + { "step_number": 1, "title": "آماده سازی", "description": "..." }, + { "step_number": 2, "title": "ترکیب", "description": "..." }, + { "step_number": 3, "title": "مصرف", "description": "..." } + ] + }, + "alternative_recommendations": [] + } +} +``` + +--- + +## نتیجه نهایی + +برای اینکه UI فعلی بدون parse کردن رشته‌ها و بدون hard-code اضافی درست کار کند، بک‌اند باید حداقل این موارد را صریح برگرداند: + +1. نام کود +2. نوع کود +3. نسبت NPK به صورت عددی و متنی +4. روش مصرف +5. فاصله مصرف +6. مقدار پایه در هکتار +7. مقدار پایه در متر مربع +8. واحد مقدار مصرف +9. استدلال توصیه +10. آنالیز ماکرو و ریزمغذی‌ها به صورت ساختارمند +11. هشدار ایمنی +12. مراحل مصرف +13. کودهای جایگزین با توضیح + +اگر خواستی، قدم بعدی می‌توانم همین فایل را به یک قرارداد نهایی هماهنگ با TypeScript interface های `src/libs/api/services/fertilizationRecommendationService.ts` هم تبدیل کنم. diff --git a/src/libs/api/services/fertilizationRecommendationService.ts b/src/libs/api/services/fertilizationRecommendationService.ts index 886d580..ffe086d 100644 --- a/src/libs/api/services/fertilizationRecommendationService.ts +++ b/src/libs/api/services/fertilizationRecommendationService.ts @@ -4,11 +4,6 @@ */ import { apiClient } from "../client"; -import type { - RecommendationTaskInitResponse, - RecommendationTaskStatusResponse, -} from "./recommendationTask"; -import { normalizeRecommendationTaskStatus } from "./recommendationTask"; const PREFIX = "/api/fertilization-recommendation"; const RECOMMEND_PREFIX = "/api/fertilization"; @@ -40,12 +35,95 @@ export interface FertilizationConfigResponse { cropOptions: CropOption[]; } -export interface FertilizationPlan { - npkRatio: string; - amountPerHectare: string; - applicationMethod: string; - applicationInterval: string; +export interface FertilizationNpkRatio { + n: number; + p: number; + k: number; + label: string; +} + +export interface FertilizationApplicationMethod { + id: string; + label: string; +} + +export interface FertilizationApplicationInterval { + label: string; + unit: string; + value: number; +} + +export interface FertilizationDosage { + label: string; + unit: string; + calculation_basis: string; + base_amount_per_hectare: number; + base_amount_per_square_meter: number; +} + +export interface FertilizationPrimaryRecommendation { + fertilizer_code: string; + fertilizer_name: string; + display_title: string; + fertilizer_type: string; reasoning: string; + summary: string; + npk_ratio: FertilizationNpkRatio; + application_method: FertilizationApplicationMethod; + application_interval: FertilizationApplicationInterval; + dosage: FertilizationDosage; +} + +export interface FertilizationNutrientItem { + key: string; + name: string; + unit: string; + description: string; + value: number; +} + +export interface FertilizationApplicationStep { + step_number: number; + title: string; + description: string; +} + +export interface FertilizationApplicationGuide { + safety_warning: string; + steps: FertilizationApplicationStep[]; +} + +export interface FertilizationAlternativeRecommendation { + fertilizer_code: string; + fertilizer_name: string; + fertilizer_type: string; + usage_method: string; + description: string; +} + +export interface FertilizationSection { + title: string; + icon: string; + type: string; + content?: string; + items?: string[]; + applicationMethod?: string; + fertilizerType?: string; + validityPeriod?: string; + amount?: string; + expandableExplanation?: string; + timing?: string; +} + +export interface FertilizationRecommendationResult { + primary_recommendation: FertilizationPrimaryRecommendation; + nutrient_analysis: { + macro: FertilizationNutrientItem[]; + micro: FertilizationNutrientItem[]; + }; + application_guide: FertilizationApplicationGuide; + alternative_recommendations: FertilizationAlternativeRecommendation[]; + sections: FertilizationSection[]; } export interface FertilizationRecommendPayload { @@ -58,17 +136,9 @@ export interface FertilizationRecommendPayload { waterEC?: string; } -export interface FertilizationRecommendationResult { - plan: FertilizationPlan; - status?: string; -} - -export type FertilizationRecommendResponse = - | FertilizationRecommendationResult - | RecommendationTaskInitResponse; - interface ApiResponse { - status: string; + code: number; + msg: string; data: T; } @@ -77,26 +147,6 @@ async function unwrap(promise: Promise>): Promise { return res.data; } -function normalizeTaskInitResponse( - task: RecommendationTaskInitResponse, -): RecommendationTaskInitResponse { - return { - ...task, - status: normalizeRecommendationTaskStatus(task.status), - }; -} - -function normalizeRecommendationResult( - result: FertilizationRecommendationResult, -): FertilizationRecommendationResult { - return result.status - ? { - ...result, - status: normalizeRecommendationTaskStatus(result.status), - } - : result; -} - export const fertilizationRecommendationService = { getConfig(farmUuid: string): Promise { return unwrap( @@ -108,39 +158,12 @@ export const fertilizationRecommendationService = { recommend( payload?: FertilizationRecommendPayload, - ): Promise { + ): Promise { return unwrap( - apiClient.post>( + apiClient.post>( `${RECOMMEND_PREFIX}/recommend/`, payload ?? {}, ), - ).then((response) => - "task_id" in response - ? normalizeTaskInitResponse(response) - : normalizeRecommendationResult(response), ); }, - - getRecommendStatus( - taskId: string, - farmUuid: string, - ): Promise< - RecommendationTaskStatusResponse - > { - return unwrap( - apiClient.get< - ApiResponse< - RecommendationTaskStatusResponse - > - >( - `${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`, - ), - ).then((response) => ({ - ...response, - status: normalizeRecommendationTaskStatus(response.status), - result: response.result - ? normalizeRecommendationResult(response.result) - : undefined, - })); - }, }; diff --git a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx index 6ad6ed3..2726c01 100644 --- a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx +++ b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx @@ -1,24 +1,36 @@ "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 Collapse from "@mui/material/Collapse"; +import Chip from "@mui/material/Chip"; import CircularProgress from "@mui/material/CircularProgress"; +import Collapse from "@mui/material/Collapse"; +import Drawer from "@mui/material/Drawer"; import IconButton from "@mui/material/IconButton"; -import { useTheme, alpha } from "@mui/material/styles"; +import LinearProgress from "@mui/material/LinearProgress"; +import Paper from "@mui/material/Paper"; +import Slider from "@mui/material/Slider"; +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 { useFarmHub } from "@/hooks/useFarmHub"; -import type { - GrowthStage, - CropOption, - FertilizationPlan, +import { + fertilizationRecommendationService, + type CropOption, + type FertilizationAlternativeRecommendation, + type FertilizationNutrientItem, + type FertilizationRecommendationResult, + type GrowthStage, } from "@/libs/api/services/fertilizationRecommendationService"; -import { fertilizationRecommendationService } from "@/libs/api/services/fertilizationRecommendationService"; -import { isRecommendationTaskRunning } from "@/libs/api/services/recommendationTask"; import { selectedPlantsService } from "@/libs/api/services/selectedPlantsService"; const GROWTH_STAGE_LABELS: Record = { @@ -36,6 +48,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 = { @@ -51,14 +64,13 @@ const formatStageLabel = (stage: string) => 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 && @@ -67,6 +79,20 @@ const getErrorMessage = (error: unknown, fallback: string) => ? error.message : fallback; +const formatNumber = (value: number) => + new Intl.NumberFormat("fa-IR", { maximumFractionDigits: 2 }).format(value); + +const formatUnitLabel = (unit: string) => { + const normalized = unit.toLowerCase(); + + if (normalized === "kg") return "کیلوگرم"; + if (normalized === "gram") return "گرم"; + if (normalized === "liter") return "لیتر"; + if (normalized === "milliliter") return "میلی لیتر"; + + return unit; +}; + export default function SmartFertilizationRecommendation() { const t = useTranslations("fertilization"); const theme = useTheme(); @@ -76,20 +102,29 @@ export default function SmartFertilizationRecommendation() { const primaryLight = theme.palette.primary.light; const primaryDark = theme.palette.primary.dark; const paperBg = theme.palette.background.paper; + const [growthStages, setGrowthStages] = useState([]); const [cropOptions, setCropOptions] = useState([]); const [configLoading, setConfigLoading] = useState(true); const [configError, setConfigError] = useState(null); - const [growthStage, setGrowthStage] = useState(""); + const [growthStage, setGrowthStage] = useState(""); const [selectedCrop, setSelectedCrop] = useState(null); - const [plan, setPlan] = 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 [detailsSheet, setDetailsSheet] = useState({ + isOpen: false, + title: "", + content: "", + type: "", + }); useEffect(() => { - setPlan(null); + setRecommendation(null); setRequestError(null); setSelectedCrop(null); setGrowthStages([]); @@ -123,75 +158,38 @@ export default function SmartFertilizationRecommendation() { id: stage, icon: getGrowthStageIcon(stage), label: formatStageLabel(stage), - })) ?? - []; + })) ?? []; setGrowthStages(stages); setGrowthStage(stages[0]?.id ?? ""); } }) - .catch((err: { message?: string }) => { - setConfigError(err?.message ?? "Failed to load plants"); + .catch((error: { message?: string }) => { + setConfigError(error?.message ?? "Failed to load plants"); }) .finally(() => setConfigLoading(false)); }, [farmUuid, t]); const handleGenerate = async () => { - if (!selectedCrop || !farmUuid) return; + if (!selectedCrop || !growthStage || !farmUuid) return; + setLoading(true); - setPlan(null); + setArea(1); + setRecommendation(null); setRequestError(null); setStatusMessage(t("generating")); setReasoningExpanded(false); + try { - const recommendation = await fertilizationRecommendationService.recommend( - { - farm_uuid: farmUuid, - crop_id: selectedCrop, - growth_stage: growthStage, - }, - ); + const response = await fertilizationRecommendationService.recommend({ + farm_uuid: farmUuid, + crop_id: selectedCrop, + growth_stage: growthStage, + }); - if ("task_id" in recommendation) { - let attempts = 0; - let taskStatus = - await fertilizationRecommendationService.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 fertilizationRecommendationService.getRecommendStatus( - recommendation.task_id, - farmUuid, - ); - } - - if (taskStatus.status === "failed" || !taskStatus.result?.plan) { - throw new Error(taskStatus.error ?? t("errors.generateFailed")); - } - - setPlan(taskStatus.result.plan); - - return; - } - - if (!recommendation.plan) { - throw new Error(t("errors.generateFailed")); - } - - setPlan(recommendation.plan); + setRecommendation(response); } catch (error) { - setPlan(null); + setRecommendation(null); setRequestError(getErrorMessage(error, t("errors.generateFailed"))); } finally { setLoading(false); @@ -199,7 +197,7 @@ export default function SmartFertilizationRecommendation() { } }; - const stageIndex = growthStages.findIndex((s) => s.id === growthStage); + const stageIndex = growthStages.findIndex((stage) => stage.id === growthStage); const selectedCropOption = cropOptions.find((option) => option.id === selectedCrop) ?? null; const selectedGrowthStage = @@ -208,6 +206,35 @@ export default function SmartFertilizationRecommendation() { selectedGrowthStage?.label ?? formatStageLabel(growthStage) }`; + const primaryRecommendation = recommendation?.primary_recommendation ?? null; + const recommendationSection = + recommendation?.sections.find((section) => section.type === "recommendation") ?? + null; + const warningSections = + recommendation?.sections.filter((section) => section.type === "warning") ?? []; + const fertilizerName = primaryRecommendation?.fertilizer_name ?? ""; + const displayTitle = + primaryRecommendation?.display_title ?? recommendationSection?.title ?? fertilizerName; + const dosageUnit = formatUnitLabel(primaryRecommendation?.dosage.unit ?? "kg"); + const baseAmountPerHectare = + primaryRecommendation?.dosage.base_amount_per_hectare ?? 0; + const baseAmountPerSquareMeter = + primaryRecommendation?.dosage.base_amount_per_square_meter ?? 0; + const totalAmount = baseAmountPerHectare * area; + const macroNutrients = recommendation?.nutrient_analysis.macro ?? []; + const microNutrients = recommendation?.nutrient_analysis.micro ?? []; + const applicationSteps = recommendation?.application_guide.steps ?? []; + const alternativeFertilizers = recommendation?.alternative_recommendations ?? []; + + const warningMessages = useMemo(() => { + const items = [ + recommendation?.application_guide.safety_warning, + ...warningSections.map((section) => section.content), + ].filter((item): item is string => Boolean(item)); + + return Array.from(new Set(items)); + }, [recommendation?.application_guide.safety_warning, warningSections]); + const handleCropSelect = (crop: CropOption) => { setSelectedCrop((prev) => { const nextCrop = prev === crop.id ? null : crop.id; @@ -222,16 +249,52 @@ export default function SmartFertilizationRecommendation() { setGrowthStages(nextStages); setGrowthStage(nextStages[0]?.id ?? ""); + setRecommendation(null); return nextCrop; }); }; const handleBackToForm = () => { - setPlan(null); + 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 openNutrientDetails = (item: FertilizationNutrientItem) => { + setDetailsSheet({ + isOpen: true, + title: item.name, + content: item.description, + type: "nutrient", + }); + }; + + const openAlternativeDetails = (item: FertilizationAlternativeRecommendation) => { + setDetailsSheet({ + isOpen: true, + title: item.fertilizer_name, + content: item.description, + type: "alternative", + }); + }; + + const closeDetailsSheet = () => { + setDetailsSheet((prev) => ({ ...prev, isOpen: false })); + }; + return ( - - {plan ? ( + + {recommendation ? ( - + {resultContext} @@ -277,99 +335,481 @@ export default function SmartFertilizationRecommendation() { - - - - - - - {t("result.title")} - - - - - - - - - - - + + + - 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) } }} - > - - + + + + {displayTitle} + + + {fertilizerName} + + + NPK: {primaryRecommendation?.npk_ratio.label} + + + + + + + + + + + مقدار پایه: {primaryRecommendation?.dosage.label ?? "-"} + + + + + + + محاسبه مقدار برای مساحت مزرعه + + + هکتار + + + + + 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(totalAmount)} {dosageUnit} + + + بر پایه {formatNumber(baseAmountPerHectare)} {dosageUnit} در هر هکتار + + + معادل {formatNumber(baseAmountPerSquareMeter)} {dosageUnit} در هر متر مربع + + + + + + + + + + + آنالیز ترکیبات + + + + {macroNutrients.map((item) => ( + openNutrientDetails(item)} + sx={{ + "&:hover": { + backgroundColor: alpha(primaryMain, 0.04), + }, + }} + > + + + {item.name} + + + {formatNumber(item.value)}% + + + + + ))} + + + - {t("result.whyRecommendation")} - - - - - - - - {plan.reasoning} + ریزمغذی ها + + + {microNutrients.length ? ( + microNutrients.map((item) => ( + openNutrientDetails(item)} + /> + )) + ) : ( + + )} + + + + + + + + + + + + + + + {t("result.title")} - + + + + + + + + + {recommendationSection?.validityPeriod && ( + + + + )} + + + 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) } }} + > + + + + {t("result.whyRecommendation")} + + + + + + + + {primaryRecommendation?.reasoning} + + + + + + + + + + + + مراحل و دستورالعمل مصرف + + + + {applicationSteps.map((item, index) => ( + + {item.title} + + + {item.description} + + + + ))} + + - - + + + + {warningMessages.length > 0 && ( + + + هشدارها و نکات مهم + + + {warningMessages.map((warning, index) => ( + + {warning} + + ))} + + + )} + + + + کودهای جایگزین + + + در صورت عدم دسترسی به پیشنهاد اصلی، می توانید از موارد زیر استفاده کنید: + + + + {alternativeFertilizers.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.fertilizer_name} + + + + روش مصرف: {item.usage_method} + + + + + + ))} + + {t("title")} - + {t("subtitle")} {!!growthStages.length && ( <> - + {t("growthStage.title")} @@ -510,12 +941,7 @@ export default function SmartFertilizationRecommendation() { )} - + {t("plantSelection.title")} {configLoading ? ( @@ -592,10 +1018,7 @@ export default function SmartFertilizationRecommendation() { background: `linear-gradient(135deg, ${alpha(primaryMain, 0.15)} 0%, ${alpha(primaryMain, 0.08)} 100%)`, }} > - + {statusMessage ?? t("generating")} @@ -604,6 +1027,50 @@ export default function SmartFertilizationRecommendation() { )} + + + + + + {detailsSheet.title} + + + + + + + {detailsSheet.content} + + + + + ); } @@ -623,6 +1090,7 @@ function CropCard({ const primaryMain = theme.palette.primary.main; const primaryDark = theme.palette.primary.dark; const paperBg = theme.palette.background.paper; + return ( - + {label} {selected && ( - + )} ); @@ -687,6 +1148,7 @@ function PrescriptionRow({ }) { const theme = useTheme(); const primaryMain = theme.palette.primary.main; + return ( - + {label}