From fa832e14b5ff22a8638a9b3d52185d8fc5dec6e5 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Tue, 28 Apr 2026 19:00:11 +0330 Subject: [PATCH] UPDATE --- ...ION_RECOMMENDATION_BACKEND_REQUIREMENTS.md | 216 +++ SMART_IRRIGATION_BACKEND_INTEGRATION.md | 534 +++++++ messages/fa.json | 130 +- src/data/dictionaries/ar.json | 94 ++ src/data/dictionaries/en.json | 94 ++ src/data/dictionaries/fa.json | 97 ++ src/data/dictionaries/fr.json | 94 ++ .../irrigationRecommendationService.ts | 101 +- .../SmartIrrigationRecommendation.tsx | 1294 ++++++++--------- 9 files changed, 1899 insertions(+), 755 deletions(-) create mode 100644 IRRIGATION_RECOMMENDATION_BACKEND_REQUIREMENTS.md create mode 100644 SMART_IRRIGATION_BACKEND_INTEGRATION.md diff --git a/IRRIGATION_RECOMMENDATION_BACKEND_REQUIREMENTS.md b/IRRIGATION_RECOMMENDATION_BACKEND_REQUIREMENTS.md new file mode 100644 index 0000000..6da6369 --- /dev/null +++ b/IRRIGATION_RECOMMENDATION_BACKEND_REQUIREMENTS.md @@ -0,0 +1,216 @@ +# Backend data needed only for irrigation recommendation + +این فایل فقط اطلاعاتی را لیست می کند که فرانت برای بخش `recommendation` در صفحه +`src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx` +از بک اند نیاز دارد. + +این سند شامل داده های `config`، `history`، یا اکشن های جانبی نیست؛ فقط داده های لازم برای +ساخت نتیجه توصیه آبیاری را پوشش می دهد. + +## Request payload + +فرانت برای گرفتن توصیه فقط به این فیلدها نیاز دارد: + +```json +{ + "farm_uuid": "string", + "plant_name": "string", + "growth_stage": "string" +} +``` + +## Response fields used by UI + +فرانت در وضعیت فعلی فقط از این فیلدها در پاسخ recommendation استفاده می کند: + +```json +{ + "plan": { + "frequencyPerWeek": 4, + "durationMinutes": 38, + "bestTimeOfDay": "05:30 تا 08:00 صبح", + "moistureLevel": 72, + "warning": "string" + }, + "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" + } + ] + }, + "timeline": [ + { + "step_number": 1, + "title": "string", + "description": "string" + } + ], + "sections": [ + { + "title": "string", + "icon": "string", + "type": "warning | tip", + "content": "string" + } + ] +} +``` + +## Field-by-field usage + +### 1) `plan` + +برای نمایش خلاصه نسخه و هشدارها: + +- `plan.frequencyPerWeek` + - نمایش تعداد نوبت آبیاری +- `plan.durationMinutes` + - نمایش مدت هر نوبت +- `plan.bestTimeOfDay` + - نمایش بهترین بازه زمانی آبیاری +- `plan.moistureLevel` + - نمایش مقدار رطوبت فعلی در gauge +- `plan.warning` + - نمایش هشدار اصلی در بخش warnings + +### 2) `water_balance` + +برای نمایش تراز آب و داده های روزانه: + +- `water_balance.daily[]` + - `forecast_date` + - `gross_irrigation_mm` + - `irrigation_timing` + - `et0_mm` + - `etc_mm` + - `effective_rainfall_mm` +- `water_balance.crop_profile` + - `kc_initial` + - `kc_mid` + - `kc_end` +- `water_balance.active_kc` + - فعلا در UI فعلی استفاده مستقیم ندارد، اما بهتر است بماند اگر قرار است بعدا نمایش داده شود + +### 3) `timeline` + +برای نمایش Stepper مراحل اجرا: + +- `timeline[].step_number` +- `timeline[].title` +- `timeline[].description` + +نکته: +- در UI فعلی فقط همین سه فیلد لازم است. +- `time_label` و `state` قبلا در ماک استفاده شده بودند ولی در نسخه فعلی UI دیگر مصرف نمی شوند. + +### 4) `sections` + +برای هشدارها و نکات تکمیلی: + +- `sections[].type` + - فقط این مقادیر فعلا مصرف می شوند: + - `warning` + - `tip` +- `sections[].title` + - برای کارت tip +- `sections[].icon` + - برای کارت tip +- `sections[].content` + - برای متن warning و tip + +## Minimum response required + +اگر بخواهیم حداقل پاسخ قابل استفاده برای UI فعلی را تعریف کنیم، این ساختار کافی است: + +```json +{ + "plan": { + "frequencyPerWeek": 4, + "durationMinutes": 38, + "bestTimeOfDay": "05:30 تا 08:00 صبح", + "moistureLevel": 72, + "warning": "در ساعات گرم روز آبیاری انجام نشود" + }, + "water_balance": { + "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" + } + ] + }, + "timeline": [ + { + "step_number": 1, + "title": "بررسی فشار", + "description": "فشار ابتدا و انتهای لاین کنترل شود" + } + ], + "sections": [ + { + "title": "هشدار تبخیر بالا", + "icon": "tabler-alert-triangle", + "type": "warning", + "content": "در ساعات گرم روز آبیاری انجام نشود" + }, + { + "title": "نکته بهره وری", + "icon": "tabler-bulb", + "type": "tip", + "content": "شست وشوی فیلترها به یکنواختی آبیاری کمک می کند" + } + ] +} +``` + +## Fields currently not required by this UI + +فیلدهای زیر در نسخه فعلی صفحه برای نمایش recommendation لازم نیستند: + +- `raw_response` +- `status` +- `generated_at` +- `recommendation_title` +- `recommendation_subtitle` +- `final_verdict` +- `primary_method` +- `usage_summary` +- `alternative_plans` +- `sections[].type = schedule` +- `sections[].type = method` + +## Practical backend recommendation + +اگر هدف فقط راه اندازی همین UI فعلی است، بهتر است endpoint recommendation حداقل این 4 بخش را برگرداند: + +- `plan` +- `water_balance` +- `timeline` +- `sections` + +و در `sections` فعلا حداقل این دو نوع را پشتیبانی کند: + +- `warning` +- `tip` + diff --git a/SMART_IRRIGATION_BACKEND_INTEGRATION.md b/SMART_IRRIGATION_BACKEND_INTEGRATION.md new file mode 100644 index 0000000..b34ecfc --- /dev/null +++ b/SMART_IRRIGATION_BACKEND_INTEGRATION.md @@ -0,0 +1,534 @@ +# Smart Irrigation Backend Integration + +این فایل مشخص می کند صفحه +`src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx` +برای کار کامل با بک‌اند دقیقا چه داده هایی لازم دارد و از کدام endpointها باید استفاده کند. + +هدف این سند: +- حذف داده های ماک +- مشخص شدن contract حداقلی بین فرانت و بک‌اند +- معلوم شدن این که هر بخش UI از کدام فیلد تغذیه می شود + +--- + +## 1) دریافت محصولات انتخاب شده مزرعه + +### Endpoint + +`GET /api/plants/selected/?farm_uuid=` + +### کاربرد در UI + +برای این بخش ها استفاده می شود: +- نمایش لیست محصول ها برای انتخاب +- گرفتن stageهای هر محصول +- انتخاب مقدار اولیه `selectedCrop` +- انتخاب مقدار اولیه `growthStage` + +### فیلدهای لازم از response + +```json +{ + "data": [ + { + "name": "گوجه فرنگی", + "icon": "tabler-carrot", + "growth_stages": ["رویشی", "گلدهی", "میوه دهی"] + } + ] +} +``` + +### فیلدهای مصرفی + +- `data[].name` + - برای `crop.id` + - برای `crop.name` +- `data[].icon` + - برای آیکن کارت محصول +- `data[].growth_stages[]` + - برای لیست stageها + +--- + +## 2) تولید recommendation آبیاری + +### Endpoint + +`POST /api/irrigation/recommend/` + +### Request payload مورد نیاز فرانت + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "گوجه فرنگی", + "growth_stage": "گلدهی" +} +``` + +### نکته مهم + +در UI فعلی برای recommendation فقط این سه فیلد برای request لازم هستند: +- `farm_uuid` +- `plant_name` +- `growth_stage` + +--- + +## 3) فیلدهای لازم از response recommendation + +فرانت در وضعیت فعلی فقط این بخش ها را برای نمایش نتیجه لازم دارد. + +### ساختار کلی مورد نیاز + +```json +{ + "code": 200, + "msg": "success", + "data": { + "recommendation_uuid": "uuid", + "crop_id": "گوجه فرنگی", + "plant_name": "گوجه فرنگی", + "growth_stage": "گلدهی", + "irrigation_method_name": "آبیاری قطره ای", + "status": "pending_confirmation", + "status_label": "منتظر تایید", + "plan": {}, + "water_balance": {}, + "timeline": [], + "sections": [] + } +} +``` + +--- + +## 4) مصرف دقیق هر فیلد در UI recommendation + +## 4.1) هدر صفحه نتیجه + +برای نوار بالای صفحه نتیجه: + +- `data.plant_name` +- `data.growth_stage` + +کاربرد: +- ساخت `resultContext` +- نمایش عنوان بالای صفحه بعد از تولید recommendation + +--- + +## 4.2) کارت Instruction Details + +این کارت از این فیلدها تغذیه می شود: + +### از `plan` +- `plan.frequencyPerWeek` +- `plan.durationMinutes` +- `plan.bestTimeOfDay` +- `plan.moistureLevel` + +### از `water_balance` +- `water_balance.daily[].gross_irrigation_mm` + +### نمونه حداقلی + +```json +{ + "plan": { + "frequencyPerWeek": 4, + "durationMinutes": 38, + "bestTimeOfDay": "05:30 تا 08:00 صبح", + "moistureLevel": 72, + "warning": "در ساعات گرم روز آبیاری انجام نشود" + }, + "water_balance": { + "daily": [ + { + "gross_irrigation_mm": 17 + } + ] + } +} +``` + +### نکته + +در UI فعلی مقدار `Water Amount` با تکیه بر: +- `water_balance.daily[0].gross_irrigation_mm` +- و مساحت انتخابی در فرانت +محاسبه می شود. + +اگر بخواهید محاسبه دقیق تر و backend-driven شود، بهتر است بک‌اند یکی از این دو را هم بدهد: +- `recommended_liters_per_hectare` +- یا `recommended_total_liters` + +اما فعلا UI بدون اینها هم کار می کند. + +--- + +## 4.3) Gauge / Circular Indicator + +برای gauge فقط این دو مقدار لازم است: + +- `plan.moistureLevel` +- `water_balance.daily` لازم نیست + +و اگر بخواهید target هم از بک‌اند بیاید، بهتر است این فیلد اضافه شود: + +- `target_moisture` + +### وضعیت فعلی + +در نسخه فعلی فرانت هنوز یک target mock-style استفاده می کند. +برای اتصال کامل بهتر است بک‌اند این فیلد را هم داخل `plan` برگرداند: + +```json +{ + "plan": { + "moistureLevel": 72, + "targetMoisture": 78 + } +} +``` + +### پیشنهاد + +اگر ممکن است این فیلد به response اضافه شود: +- `plan.targetMoisture` + +--- + +## 4.4) تراز آب و نیاز روزانه + +این بخش مستقیما از `water_balance` استفاده می کند. + +### فیلدهای لازم + +```json +{ + "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" + } + ] + } +} +``` + +### فیلدهای مصرفی + +#### از `water_balance.daily[]` +- `forecast_date` +- `gross_irrigation_mm` +- `irrigation_timing` +- `et0_mm` +- `etc_mm` +- `effective_rainfall_mm` + +#### از `water_balance.crop_profile` +- `kc_initial` +- `kc_mid` +- `kc_end` + +#### از `water_balance` +- `active_kc` + - فعلا در UI اصلی مستقیم رندر نمی شود، ولی برای آینده مفید است + +--- + +## 4.5) Stepper مراحل و دستورالعمل اجرا + +برای این بخش فقط `timeline` لازم است. + +### ساختار لازم + +```json +{ + "timeline": [ + { + "step_number": 1, + "title": "بررسی فشار", + "description": "فشار ابتدا و انتهای لاین کنترل شود" + } + ] +} +``` + +### فیلدهای مصرفی + +- `timeline[].step_number` +- `timeline[].title` +- `timeline[].description` + +### نکته + +در UI فعلی این فیلدها کافی هستند. +فیلدهایی مثل `time_label` یا `state` فعلا لازم نیستند. + +--- + +## 4.6) هشدارها و نکات + +این بخش از `plan.warning` و `sections` استفاده می کند. + +### فیلدهای لازم + +```json +{ + "plan": { + "warning": "در ساعات گرم روز آبیاری انجام نشود" + }, + "sections": [ + { + "title": "هشدار تبخیر بالا", + "icon": "tabler-alert-triangle", + "type": "warning", + "content": "در ساعات گرم روز آبیاری انجام نشود" + }, + { + "title": "نکته بهره وری", + "icon": "tabler-bulb", + "type": "tip", + "content": "شست وشوی فیلترها به یکنواختی آبیاری کمک می کند" + } + ] +} +``` + +### مصرف در UI + +#### هشدارها +- `plan.warning` +- `sections[].type === "warning"` +- `sections[].content` + +#### نکات پایین صفحه +- `sections[].type === "tip"` +- `sections[].title` +- `sections[].icon` +- `sections[].content` + +--- + +## 5) حداقل response پیشنهادی برای recommendation + +اگر هدف فقط راه افتادن UI فعلی recommendation باشد، این حداقل پاسخ کافی است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "recommendation_uuid": "8a4c22d8-3f75-4aef-8e04-b40f6b4a2d11", + "crop_id": "گوجه فرنگی", + "plant_name": "گوجه فرنگی", + "growth_stage": "گلدهی", + "irrigation_method_name": "آبیاری قطره ای", + "status": "pending_confirmation", + "status_label": "منتظر تایید", + "plan": { + "frequencyPerWeek": 4, + "durationMinutes": 38, + "bestTimeOfDay": "05:30 تا 08:00 صبح", + "moistureLevel": 72, + "targetMoisture": 78, + "warning": "در ساعات گرم روز آبیاری انجام نشود" + }, + "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" + } + ] + }, + "timeline": [ + { + "step_number": 1, + "title": "بررسی فشار", + "description": "فشار ابتدا و انتهای لاین کنترل شود" + } + ], + "sections": [ + { + "title": "هشدار تبخیر بالا", + "icon": "tabler-alert-triangle", + "type": "warning", + "content": "در ساعات گرم روز آبیاری انجام نشود" + }, + { + "title": "نکته بهره وری", + "icon": "tabler-bulb", + "type": "tip", + "content": "شست وشوی فیلترها به یکنواختی آبیاری کمک می کند" + } + ] + } +} +``` + +--- + +## 6) history recommendationهای آبیاری + +### Endpoint + +`GET /api/irrigation/recommendations/?farm_uuid=&page=1&page_size=10` + +### کاربرد در UI + +برای جدول history پایین فرم استفاده می شود. + +### فیلدهای لازم هر آیتم + +```json +{ + "recommendation_uuid": "uuid", + "plant_name": "گوجه فرنگی", + "growth_stage": "گلدهی", + "irrigation_method_name": "آبیاری قطره ای", + "status": "pending_confirmation", + "status_label": "منتظر تایید", + "requested_at": "2025-02-12T09:30:00Z" +} +``` + +### نکته مهم برای فرانت فعلی + +در کد فعلی جدول history این فیلد را به عنوان ستون روش آبیاری می خواهد: + +- `irrigation_method` + +اما طبق contract بک‌اند، فیلد موجود این است: + +- `irrigation_method_name` + +### نتیجه + +برای اتصال کامل یکی از این دو کار باید انجام شود: + +1. یا فرانت `irrigation_method_name` را بخواند +2. یا بک‌اند علاوه بر آن، فیلد `irrigation_method` هم برگرداند + +پیشنهاد بهتر: +- فرانت را با `irrigation_method_name` align کنید + +### pagination لازم + +```json +{ + "pagination": { + "page": 1, + "page_size": 10, + "total_pages": 1, + "total_items": 1, + "has_next": false, + "has_previous": false, + "next": null, + "previous": null + } +} +``` + +--- + +## 7) جزئیات یک recommendation + +### Endpoint + +`GET /api/irrigation/recommendations/{recommendation_uuid}/` + +### کاربرد در UI + +وقتی کاربر از جدول history روی آیکن گزارش کلیک می کند، این endpoint باید صدا زده شود. + +### ساختار لازم + +خروجی این endpoint باید همان shape endpoint recommendation اصلی را برگرداند؛ یعنی: + +- `recommendation_uuid` +- `plant_name` +- `growth_stage` +- `irrigation_method_name` +- `status` +- `status_label` +- `plan` +- `water_balance` +- `timeline` +- `sections` + +--- + +## 8) فیلدهایی که فعلا در UI لازم نیستند + +این فیلدها در نسخه فعلی صفحه ضروری نیستند: + +- `raw_response` +- `crop_id` (اگر `plant_name` وجود داشته باشد) +- `sensor_uuid` +- `status` در خود recommendation result page +- `generated_at` +- `final_verdict` +- `recommendation_title` +- `recommendation_subtitle` +- `primary_method` +- `usage_summary` +- `alternative_plans` +- `sections[].type = schedule` +- `sections[].type = method` + +--- + +## 9) جمع بندی نهایی + +برای این صفحه اگر بخواهیم فقط داده های ضروری از بک‌اند بگیریم، این endpointها کافی هستند: + +1. `GET /api/plants/selected/` +2. `POST /api/irrigation/recommend/` +3. `GET /api/irrigation/recommendations/` +4. `GET /api/irrigation/recommendations/{recommendation_uuid}/` + +و برای recommendation page حداقل این بخش های response باید حتما وجود داشته باشند: + +- `plant_name` +- `growth_stage` +- `plan` +- `water_balance` +- `timeline` +- `sections` + +و برای history: + +- `recommendation_uuid` +- `plant_name` +- `growth_stage` +- `irrigation_method_name` +- `status` +- `status_label` +- `requested_at` +- `pagination` + diff --git a/messages/fa.json b/messages/fa.json index 02cf21d..7a1b4dc 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -627,21 +627,21 @@ "description": "این شبیه‌ساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی محاسبه می‌کند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی در باد نمایش داده می‌شود. محصول‌دهی (g) پس از ۲۰٪ رشد شروع شده و با تعداد برگ، نور و آب شتاب می‌گیرد. سرعت محصول (g/s) نشان‌دهنده نرخ لحظه‌ای تولید است. نمودار تغییرات همه شاخص‌ها را در طول زمان ثبت می‌کند." }, "irrigation": { - "title": "توصیه هوشمند آبیاری", - "subtitle": "بر اساس داده‌های ثبت شده مزرعه شما", - "farmInfo": { - "title": "اطلاعات مزرعه", - "soilType": "نوع خاک", - "waterQuality": "کیفیت آب", - "climateZone": "منطقه اقلیمی" - }, - "verifiedBadge": "داده تأیید شده", - "editFarmInfo": "ویرایش اطلاعات مزرعه", + "title": "توصیه آبیاری هوشمند", + "subtitle": "محصول و مرحله رشد را انتخاب کنید تا نسخه آبیاری این مزرعه از روی داده های بک اند تولید شود.", + "farmIrrigationMethod": "روش آبیاری ثبت شده روی مزرعه: {method}", "plantSelection": { "title": "انتخاب محصول" }, "growthStage": { - "title": "مرحله رشد" + "title": "مرحله رشد", + "options": { + "initial": "شروع رشد", + "vegetative": "رشد رویشی", + "flowering": "گلدهی", + "fruiting": "باردهی", + "maturity": "رسیدگی" + } }, "crops": { "wheat": "گندم", @@ -649,28 +649,98 @@ "cotton": "پنبه", "saffron": "زعفران", "canola": "کلزا", - "vegetables": "سبزیجات" + "vegetables": "سبزیجات", + "cucumber": "خیار" }, - "generateCta": "تولید برنامه آبیاری", - "generating": "در حال تحلیل و تولید برنامه آبیاری...", + "units": { + "mm": "میلی متر", + "literShort": "لیتر" + }, + "status": { + "fetchingReport": "در حال دریافت گزارش توصیه" + }, + "actions": { + "saveVersion": "ذخیره این نسخه", + "setReminder": "تنظیم یادآور" + }, + "gauge": { + "currentSoilMoisture": "رطوبت فعلی خاک", + "target": "هدف" + }, + "instructionStats": { + "waterAmount": { + "label": "مقدار آب", + "hint": "برای سطح انتخاب شده" + }, + "duration": { + "label": "مدت زمان", + "hint": "زمان تقریبی هر نوبت آبیاری" + }, + "frequency": { + "label": "تناوب", + "hint": "تعداد دفعات پیشنهادی بر اساس تراز آب و مرحله رشد" + }, + "bestWindow": { + "label": "بهترین بازه", + "hint": "بهترین زمان اجرا برای کاهش تلفات تبخیر" + } + }, + "recommendation": { + "instructionDetailsTitle": "جزئیات دستور آبیاری", + "instructionDetailsSubtitle": "خلاصه ای سریع از دستور اجرایی آبیاری و شاخص اصلی موثر بر این توصیه.", + "soilMoistureDriverTitle": "محرک رطوبت خاک", + "soilMoistureDriverSubtitle": "رطوبت فعلی خاک با مقدار هدف مقایسه می شود تا منطق توصیه مشخص باشد.", + "waterBalanceTitle": "تراز آب و نیاز روزانه", + "executionWindow": "بازه اجرا", + "cropProfileTitle": "پروفایل گیاه", + "kcInitial": "Kc شروع", + "kcMid": "Kc میانی", + "kcEnd": "Kc پایانی", + "executionStepsTitle": "مراحل و دستورالعمل اجرا", + "warningsTitle": "هشدارها و نکات مهم", + "generatedTitle": "نسخه آبیاری هوشمند {plant}", + "generatedSubtitle": "پیشنهاد ویژه برای مرحله {stage}", + "durationPerTurn": "{value} دقیقه در هر نوبت", + "frequencyPerWeek": "{value} نوبت در هفته", + "weatherBasedWindow": "طبق وضعیت آب و هوا" + }, + "history": { + "title": "تاریخچه توصیه های آبیاری", + "subtitle": "همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید.", + "viewReport": "مشاهده گزارش", + "empty": "هنوز توصیه ای برای این مزرعه ثبت نشده است.", + "rowsPerPage": "تعداد در صفحه:", + "displayedRows": "{from}–{to} از {count}", + "statuses": { + "pending": "در انتظار", + "in_progress": "در حال پردازش", + "completed": "تکمیل شده" + }, + "columns": { + "requestedAt": "تاریخ ثبت", + "cropStage": "محصول / مرحله رشد", + "method": "روش آبیاری", + "status": "وضعیت", + "report": "گزارش" + } + }, + "details": { + "waterBalanceTitle": "جزئیات تراز آب {date}", + "waterBalanceContent": "ET0: {et0} {mm}\nETc: {etc} {mm}\nبارش موثر: {rainfall} {mm}\nآبیاری ناخالص: {gross} {mm}\nبازه اجرا: {timing}", + "addReminder": "افزودن به یادآور", + "understood": "متوجه شدم" + }, + "generateCta": "تولید توصیه آبیاری", + "generating": "در حال تولید توصیه آبیاری...", + "loadingCard": "در حال تحلیل و تولید نسخه آبیاری...", "errors": { "noFarm": "ابتدا یک مزرعه انتخاب کنید.", - "generateFailed": "دریافت برنامه آبیاری با خطا مواجه شد.", - "timeout": "دریافت نتیجه بیش از حد طول کشید. دوباره تلاش کنید." - }, - "result": { - "moistureLevel": "سطح رطوبت هدف", - "frequency": "تناوب آبیاری", - "timesPerWeek": "بار در هفته", - "duration": "مدت هر نوبت", - "minutes": "دقیقه", - "bestTime": "بهترین زمان آبیاری", - "smartWarning": "هشدار هوشمند", - "waterBalance": "جزئیات تراز آب", - "forecastDate": "تاریخ پیش‌بینی", - "grossIrrigation": "نیاز آبیاری ناخالص", - "irrigationTiming": "زمان آبیاری", - "activeKc": "ضریب رشد فعال" + "noSelectedPlants": "برای این مزرعه هنوز محصولی انتخاب نشده است.", + "selectedPlantsFailed": "خطا در دریافت محصولات انتخاب شده مزرعه", + "generateFailed": "خطا در تولید توصیه آبیاری", + "historyFailed": "خطا در دریافت تاریخچه توصیه های آبیاری", + "reportFailed": "خطا در دریافت گزارش توصیه آبیاری", + "pendingResponse": "درخواست ثبت شد اما پاسخ نهایی هنوز آماده نیست." } }, "fertilization": { diff --git a/src/data/dictionaries/ar.json b/src/data/dictionaries/ar.json index 1f2c788..53a9f90 100644 --- a/src/data/dictionaries/ar.json +++ b/src/data/dictionaries/ar.json @@ -112,5 +112,99 @@ "economicOverview": "النظرة الاقتصادية", "sensorSection": "المستشعرات", "sensor7In1": "مستشعر التربة 7 في 1" + }, + "irrigation": { + "title": "توصية الري الذكية", + "subtitle": "اختر المحصول ومرحلة النمو لتوليد توصية الري لهذه المزرعة اعتمادا على بيانات الخلفية.", + "generateCta": "إنشاء توصية الري", + "generating": "جار إنشاء توصية الري...", + "loadingCard": "جار تحليل البيانات وإنشاء خطة الري...", + "farmIrrigationMethod": "طريقة الري المسجلة في المزرعة: {method}", + "growthStage": { + "title": "مرحلة النمو" + }, + "plantSelection": { + "title": "اختيار المحصول" + }, + "units": { + "mm": "مم", + "literShort": "لتر" + }, + "status": { + "fetchingReport": "جار تحميل تقرير التوصية" + }, + "actions": { + "saveVersion": "حفظ هذه النسخة", + "setReminder": "تعيين تذكير" + }, + "gauge": { + "currentSoilMoisture": "رطوبة التربة الحالية", + "target": "الهدف" + }, + "instructionStats": { + "waterAmount": { + "label": "كمية المياه", + "hint": "للمساحة المحددة" + }, + "duration": { + "label": "المدة", + "hint": "المدة التقديرية لكل دورة ري" + }, + "frequency": { + "label": "التكرار", + "hint": "الوتيرة المقترحة حسب توازن المياه ومرحلة النمو" + }, + "bestWindow": { + "label": "أفضل وقت", + "hint": "الوقت المفضل للتنفيذ لتقليل فقدان التبخر" + } + }, + "recommendation": { + "instructionDetailsTitle": "تفاصيل تعليمات الري", + "instructionDetailsSubtitle": "ملخص سريع لتعليمات الري الدقيقة والمؤشر الأساسي الذي يقود هذه التوصية.", + "soilMoistureDriverTitle": "مؤشر رطوبة التربة", + "soilMoistureDriverSubtitle": "تتم مقارنة رطوبة التربة الحالية بالحد المستهدف لتوضيح منطق التوصية.", + "waterBalanceTitle": "توازن المياه والاحتياج اليومي", + "executionWindow": "نافذة التنفيذ", + "cropProfileTitle": "ملف المحصول", + "kcInitial": "Kc البداية", + "kcMid": "Kc الوسط", + "kcEnd": "Kc النهاية", + "executionStepsTitle": "خطوات التنفيذ والتعليمات", + "warningsTitle": "تحذيرات وملاحظات مهمة", + "generatedTitle": "خطة الري الذكية لـ {plant}", + "generatedSubtitle": "توصية خاصة لمرحلة {stage}", + "durationPerTurn": "{value} دقيقة لكل دورة", + "frequencyPerWeek": "{value} مرات في الأسبوع", + "weatherBasedWindow": "بحسب حالة الطقس" + }, + "history": { + "title": "سجل توصيات الري", + "subtitle": "اعرض جميع التوصيات السابقة لهذه المزرعة وافتح التقرير الكامل لكل واحدة.", + "viewReport": "عرض التقرير", + "empty": "لم يتم تسجيل أي توصية لهذه المزرعة بعد.", + "columns": { + "requestedAt": "تاريخ الطلب", + "cropStage": "المحصول / مرحلة النمو", + "method": "طريقة الري", + "status": "الحالة", + "report": "التقرير" + } + }, + "details": { + "waterBalanceTitle": "تفاصيل توازن المياه {date}", + "waterBalanceContent": "ET0: {et0} {mm}\nETc: {etc} {mm}\nالأمطار الفعالة: {rainfall} {mm}\nالري الإجمالي: {gross} {mm}\nنافذة التنفيذ: {timing}", + "addReminder": "إضافة إلى التذكير", + "understood": "فهمت" + }, + "errors": { + "noFarm": "يرجى اختيار مزرعة أولا.", + "noSelectedPlants": "لم يتم اختيار أي محصول لهذه المزرعة بعد.", + "selectedPlantsFailed": "فشل تحميل المحاصيل المختارة للمزرعة", + "generateFailed": "فشل إنشاء توصية الري", + "historyFailed": "فشل تحميل سجل توصيات الري", + "reportFailed": "فشل تحميل تقرير توصية الري", + "pendingResponse": "تم تسجيل الطلب ولكن الاستجابة النهائية ليست جاهزة بعد." + } } } diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index e76778d..2bab722 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -112,5 +112,99 @@ "economicOverview": "Economic Overview", "sensorSection": "Sensors", "sensor7In1": "Soil Sensor 7-in-1" + }, + "irrigation": { + "title": "Smart Irrigation Recommendation", + "subtitle": "Select the crop and growth stage to generate the irrigation recommendation for this farm from backend data.", + "generateCta": "Generate Irrigation Recommendation", + "generating": "Generating irrigation recommendation...", + "loadingCard": "Analyzing data and generating the irrigation plan...", + "farmIrrigationMethod": "Irrigation method saved on the farm: {method}", + "growthStage": { + "title": "Growth Stage" + }, + "plantSelection": { + "title": "Plant Selection" + }, + "units": { + "mm": "mm", + "literShort": "L" + }, + "status": { + "fetchingReport": "Fetching recommendation report" + }, + "actions": { + "saveVersion": "Save This Version", + "setReminder": "Set Reminder" + }, + "gauge": { + "currentSoilMoisture": "CURRENT SOIL MOISTURE", + "target": "Target" + }, + "instructionStats": { + "waterAmount": { + "label": "Water Amount", + "hint": "across the selected field area" + }, + "duration": { + "label": "Duration", + "hint": "Estimated runtime for each irrigation cycle" + }, + "frequency": { + "label": "Frequency", + "hint": "Recommended cadence based on water balance and growth stage" + }, + "bestWindow": { + "label": "Best Window", + "hint": "Preferred execution time to reduce evaporation losses" + } + }, + "recommendation": { + "instructionDetailsTitle": "Instruction Details", + "instructionDetailsSubtitle": "A quick operational snapshot of the exact irrigation instructions and the metric driving this recommendation.", + "soilMoistureDriverTitle": "Soil Moisture Driver", + "soilMoistureDriverSubtitle": "Current soil moisture is compared against the target threshold to justify the final recommendation.", + "waterBalanceTitle": "Water Balance and Daily Need", + "executionWindow": "Execution Window", + "cropProfileTitle": "Crop Profile", + "kcInitial": "Kc Initial", + "kcMid": "Kc Mid", + "kcEnd": "Kc End", + "executionStepsTitle": "Execution Steps and Instructions", + "warningsTitle": "Warnings and Important Notes", + "generatedTitle": "Smart irrigation plan for {plant}", + "generatedSubtitle": "Special recommendation for the {stage} stage", + "durationPerTurn": "{value} minutes per cycle", + "frequencyPerWeek": "{value} times per week", + "weatherBasedWindow": "Based on weather conditions" + }, + "history": { + "title": "Irrigation Recommendation History", + "subtitle": "Review all previous recommendations for this farm and open the full report for each one.", + "viewReport": "View Report", + "empty": "No recommendations have been recorded for this farm yet.", + "columns": { + "requestedAt": "Requested At", + "cropStage": "Crop / Growth Stage", + "method": "Irrigation Method", + "status": "Status", + "report": "Report" + } + }, + "details": { + "waterBalanceTitle": "Water Balance Details {date}", + "waterBalanceContent": "ET0: {et0} {mm}\nETc: {etc} {mm}\nEffective rainfall: {rainfall} {mm}\nGross irrigation: {gross} {mm}\nExecution window: {timing}", + "addReminder": "Add to Reminder", + "understood": "Understood" + }, + "errors": { + "noFarm": "Please select a farm first.", + "noSelectedPlants": "No plants have been selected for this farm yet.", + "selectedPlantsFailed": "Failed to load selected plants for the farm", + "generateFailed": "Failed to generate irrigation recommendation", + "historyFailed": "Failed to load irrigation recommendation history", + "reportFailed": "Failed to load irrigation recommendation report", + "pendingResponse": "The request was submitted, but the final response is not ready yet." + } } } diff --git a/src/data/dictionaries/fa.json b/src/data/dictionaries/fa.json index bfdb9de..a3f827f 100644 --- a/src/data/dictionaries/fa.json +++ b/src/data/dictionaries/fa.json @@ -112,5 +112,102 @@ "economicOverview": "نمای اقتصادی", "sensorSection": "سنسورها", "sensor7In1": "سنسور خاک 7 در 1" + }, + "irrigation": { + "title": "توصیه آبیاری هوشمند", + "subtitle": "محصول و مرحله رشد را انتخاب کنید تا نسخه آبیاری این مزرعه از روی داده های بک اند تولید شود.", + "generateCta": "تولید توصیه آبیاری", + "generating": "در حال تولید توصیه آبیاری...", + "loadingCard": "در حال تحلیل و تولید نسخه آبیاری...", + "farmIrrigationMethod": "روش آبیاری ثبت شده روی مزرعه: {method}", + "growthStage": { + "title": "مرحله رشد" + }, + "plantSelection": { + "title": "انتخاب محصول" + }, + "units": { + "mm": "میلی متر", + "literShort": "لیتر" + }, + "status": { + "fetchingReport": "در حال دریافت گزارش توصیه" + }, + "actions": { + "saveVersion": "ذخیره این نسخه", + "setReminder": "تنظیم یادآور" + }, + "gauge": { + "currentSoilMoisture": "رطوبت فعلی خاک", + "target": "هدف", + "currentValueExample": "۴۲٪", + "targetValueExample": "۴۲٪" + }, + "instructionStats": { + "waterAmount": { + "label": "مقدار آب", + "hint": "برای سطح انتخاب شده" + }, + "duration": { + "label": "مدت زمان", + "hint": "زمان تقریبی هر نوبت آبیاری" + }, + "frequency": { + "label": "تناوب", + "hint": "تعداد دفعات پیشنهادی بر اساس تراز آب و مرحله رشد" + }, + "bestWindow": { + "label": "بهترین بازه", + "hint": "بهترین زمان اجرا برای کاهش تلفات تبخیر", + "example": "اوایل صبح یا نزدیک غروب" + } + }, + "recommendation": { + "instructionDetailsTitle": "جزئیات دستور آبیاری", + "instructionDetailsSubtitle": "خلاصه ای سریع از دستور اجرایی آبیاری و شاخص اصلی موثر بر این توصیه.", + "soilMoistureDriverTitle": "محرک رطوبت خاک", + "soilMoistureDriverSubtitle": "رطوبت فعلی خاک با مقدار هدف مقایسه می شود تا منطق توصیه مشخص باشد.", + "waterBalanceTitle": "تراز آب و نیاز روزانه", + "executionWindow": "بازه اجرا", + "cropProfileTitle": "پروفایل گیاه", + "kcInitial": "Kc شروع", + "kcMid": "Kc میانی", + "kcEnd": "Kc پایانی", + "executionStepsTitle": "مراحل و دستورالعمل اجرا", + "warningsTitle": "هشدارها و نکات مهم", + "generatedTitle": "نسخه آبیاری هوشمند {plant}", + "generatedSubtitle": "پیشنهاد ویژه برای مرحله {stage}", + "durationPerTurn": "{value} دقیقه در هر نوبت", + "frequencyPerWeek": "{value} نوبت در هفته", + "weatherBasedWindow": "طبق وضعیت آب و هوا" + }, + "history": { + "title": "تاریخچه توصیه های آبیاری", + "subtitle": "همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید.", + "viewReport": "مشاهده گزارش", + "empty": "هنوز توصیه ای برای این مزرعه ثبت نشده است.", + "columns": { + "requestedAt": "تاریخ ثبت", + "cropStage": "محصول / مرحله رشد", + "method": "روش آبیاری", + "status": "وضعیت", + "report": "گزارش" + } + }, + "details": { + "waterBalanceTitle": "جزئیات تراز آب {date}", + "waterBalanceContent": "ET0: {et0} {mm}\nETc: {etc} {mm}\nبارش موثر: {rainfall} {mm}\nآبیاری ناخالص: {gross} {mm}\nبازه اجرا: {timing}", + "addReminder": "افزودن به یادآور", + "understood": "متوجه شدم" + }, + "errors": { + "noFarm": "ابتدا یک مزرعه انتخاب کنید.", + "noSelectedPlants": "برای این مزرعه هنوز محصولی انتخاب نشده است.", + "selectedPlantsFailed": "خطا در دریافت محصولات انتخاب شده مزرعه", + "generateFailed": "خطا در تولید توصیه آبیاری", + "historyFailed": "خطا در دریافت تاریخچه توصیه های آبیاری", + "reportFailed": "خطا در دریافت گزارش توصیه آبیاری", + "pendingResponse": "درخواست ثبت شد اما پاسخ نهایی هنوز آماده نیست." + } } } diff --git a/src/data/dictionaries/fr.json b/src/data/dictionaries/fr.json index e584f0b..2c4e2ca 100644 --- a/src/data/dictionaries/fr.json +++ b/src/data/dictionaries/fr.json @@ -112,5 +112,99 @@ "economicOverview": "Aperçu économique", "sensorSection": "Capteurs", "sensor7In1": "Capteur de sol 7-en-1" + }, + "irrigation": { + "title": "Recommandation intelligente d'irrigation", + "subtitle": "Selectionnez la culture et le stade de croissance pour generer la recommandation d'irrigation de cette ferme a partir des donnees du backend.", + "generateCta": "Generer la recommandation", + "generating": "Generation de la recommandation d'irrigation...", + "loadingCard": "Analyse des donnees et generation du plan d'irrigation...", + "farmIrrigationMethod": "Methode d'irrigation enregistree sur la ferme : {method}", + "growthStage": { + "title": "Stade de croissance" + }, + "plantSelection": { + "title": "Selection de la culture" + }, + "units": { + "mm": "mm", + "literShort": "L" + }, + "status": { + "fetchingReport": "Recuperation du rapport de recommandation" + }, + "actions": { + "saveVersion": "Enregistrer cette version", + "setReminder": "Definir un rappel" + }, + "gauge": { + "currentSoilMoisture": "HUMIDITE ACTUELLE DU SOL", + "target": "Objectif" + }, + "instructionStats": { + "waterAmount": { + "label": "Quantite d'eau", + "hint": "sur la surface selectionnee" + }, + "duration": { + "label": "Duree", + "hint": "Duree estimee pour chaque cycle d'irrigation" + }, + "frequency": { + "label": "Frequence", + "hint": "Rythme recommande selon le bilan hydrique et le stade de croissance" + }, + "bestWindow": { + "label": "Meilleure plage", + "hint": "Horaire prefere pour reduire les pertes par evaporation" + } + }, + "recommendation": { + "instructionDetailsTitle": "Details des instructions", + "instructionDetailsSubtitle": "Un apercu rapide des instructions d'irrigation et de l'indicateur qui pilote cette recommandation.", + "soilMoistureDriverTitle": "Indicateur d'humidite du sol", + "soilMoistureDriverSubtitle": "L'humidite actuelle du sol est comparee au seuil cible pour justifier la recommandation finale.", + "waterBalanceTitle": "Bilan hydrique et besoin quotidien", + "executionWindow": "Fenetre d'execution", + "cropProfileTitle": "Profil de la culture", + "kcInitial": "Kc initial", + "kcMid": "Kc milieu", + "kcEnd": "Kc final", + "executionStepsTitle": "Etapes et instructions d'execution", + "warningsTitle": "Avertissements et notes importantes", + "generatedTitle": "Plan d'irrigation intelligent pour {plant}", + "generatedSubtitle": "Recommandation speciale pour le stade {stage}", + "durationPerTurn": "{value} minutes par cycle", + "frequencyPerWeek": "{value} fois par semaine", + "weatherBasedWindow": "Selon les conditions meteo" + }, + "history": { + "title": "Historique des recommandations d'irrigation", + "subtitle": "Consultez toutes les recommandations precedentes de cette ferme et ouvrez le rapport complet de chacune.", + "viewReport": "Voir le rapport", + "empty": "Aucune recommandation n'a encore ete enregistree pour cette ferme.", + "columns": { + "requestedAt": "Date", + "cropStage": "Culture / Stade", + "method": "Methode d'irrigation", + "status": "Statut", + "report": "Rapport" + } + }, + "details": { + "waterBalanceTitle": "Details du bilan hydrique {date}", + "waterBalanceContent": "ET0 : {et0} {mm}\nETc : {etc} {mm}\nPluie efficace : {rainfall} {mm}\nIrrigation brute : {gross} {mm}\nFenetre d'execution : {timing}", + "addReminder": "Ajouter au rappel", + "understood": "Compris" + }, + "errors": { + "noFarm": "Veuillez d'abord selectionner une ferme.", + "noSelectedPlants": "Aucune culture n'a encore ete selectionnee pour cette ferme.", + "selectedPlantsFailed": "Echec du chargement des cultures selectionnees de la ferme", + "generateFailed": "Echec de la generation de la recommandation d'irrigation", + "historyFailed": "Echec du chargement de l'historique des recommandations d'irrigation", + "reportFailed": "Echec du chargement du rapport de recommandation d'irrigation", + "pendingResponse": "La requete a ete enregistree, mais la reponse finale n'est pas encore prete." + } } } diff --git a/src/libs/api/services/irrigationRecommendationService.ts b/src/libs/api/services/irrigationRecommendationService.ts index 1b50ab6..97f8dbc 100644 --- a/src/libs/api/services/irrigationRecommendationService.ts +++ b/src/libs/api/services/irrigationRecommendationService.ts @@ -41,16 +41,19 @@ export interface IrrigationConfigResponse { } export interface IrrigationPlan { - frequencyPerWeek: number | string; - durationMinutes: number | string; - bestTimeOfDay: string; - moistureLevel: number | string; + frequencyPerWeek?: number | string; + durationMinutes?: number | string; + bestTimeOfDay?: string; + moistureLevel?: number | string; warning?: string; + [key: string]: unknown; } export interface IrrigationRecommendPayload { - farm_uuid: string; + farm_uuid?: string; + sensor_uuid?: string; crop_id?: string; + plant_name?: string; growth_stage?: string; irrigation_method_id?: string; farm_data?: Partial; @@ -78,13 +81,57 @@ export interface WaterBalance { daily: WaterBalanceDailyEntry[]; crop_profile?: WaterBalanceCropProfile; active_kc?: number; + [key: string]: unknown; +} + +export interface IrrigationRecommendationTimelineItem { + step_number: number; + title: string; + description: string; +} + +export interface IrrigationRecommendationSection { + title: string; + icon?: string; + type: "schedule" | "warning" | "tip" | "method" | string; + content: string; } export interface IrrigationRecommendationResult { + recommendation_uuid?: string; + crop_id?: string; + plant_name?: string; + growth_stage?: string; + irrigation_method_name?: string; + status?: string; + status_label?: string; plan: IrrigationPlan; raw_response?: string; water_balance?: WaterBalance; - status?: string; + timeline?: IrrigationRecommendationTimelineItem[]; + sections?: IrrigationRecommendationSection[]; +} + +export interface IrrigationRecommendationHistoryItem { + recommendation_uuid: string; + crop_id?: string; + plant_name?: string; + growth_stage?: string; + irrigation_method_name?: string; + status: "pending_confirmation" | "in_progress" | "completed" | "error" | string; + status_label: string; + requested_at: string; +} + +export interface IrrigationRecommendationHistoryPagination { + page: number; + page_size: number; + total_pages: number; + total_items: number; + has_next: boolean; + has_previous: boolean; + next: string | null; + previous: string | null; } export interface WaterStressPayload { @@ -120,10 +167,16 @@ export type IrrigationRecommendResponse = | RecommendationTaskInitResponse; interface ApiResponse { - status: string; + code?: number; + msg?: string; + status?: string; data: T; } +interface PaginatedApiResponse extends ApiResponse { + pagination: IrrigationRecommendationHistoryPagination; +} + async function unwrap(promise: Promise>): Promise { const res = await promise; return res.data; @@ -142,9 +195,7 @@ function normalizeRecommendationResult( export const irrigationRecommendationService = { listMethods(): Promise { - return unwrap( - apiClient.get>(`${PREFIX}/`), - ); + return unwrap(apiClient.get>(`${PREFIX}/`)); }, getConfig(farmUuid: string): Promise { @@ -173,6 +224,36 @@ export const irrigationRecommendationService = { ); }, + async getRecommendationsHistory( + farmUuid: string, + page = 1, + pageSize = 10, + ): Promise<{ + data: IrrigationRecommendationHistoryItem[]; + pagination: IrrigationRecommendationHistoryPagination; + }> { + 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)}/`, + ), + ).then(normalizeRecommendationResult); + }, + getWaterStress(payload?: WaterStressPayload): Promise { return unwrap( apiClient.post>( diff --git a/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx index 9736c3d..fb51748 100644 --- a/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx +++ b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { useTranslations } from "next-intl"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useLocale, useTranslations } from "next-intl"; import Alert from "@mui/material/Alert"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -9,12 +9,10 @@ import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; 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 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"; @@ -27,22 +25,20 @@ 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 { +import { useFarmHub } from "@/hooks/useFarmHub"; +import { + irrigationRecommendationService, CropOption, + IrrigationRecommendationHistoryItem, + IrrigationRecommendationHistoryPagination, IrrigationRecommendationResult, + IrrigationRecommendationSection, + IrrigationRecommendationTimelineItem, WaterBalanceDailyEntry, } from "@/libs/api/services/irrigationRecommendationService"; - -const GROWTH_STAGE_LABELS: Record = { - initial: "شروع رشد", - vegetative: "رشد رویشی", - flowering: "گلدهی", - fruiting: "باردهی", - maturity: "رسیدگی", -}; +import { selectedPlantsService } from "@/libs/api/services/selectedPlantsService"; const PLANT_ICON_MAP: Record = { corn: "tabler-plant-2", @@ -60,6 +56,12 @@ const GROWTH_STAGE_ICON_MAP: Record = { flowering: "tabler-flower", fruiting: "tabler-apple", maturity: "tabler-basket", + "شروع رشد": "tabler-seedling", + "رویشی": "tabler-leaf", + "گلدهی": "tabler-flower", + "میوه دهی": "tabler-apple", + "میوه‌دهی": "tabler-apple", + "رسیدگی": "tabler-basket", }; type GrowthStage = { @@ -68,27 +70,7 @@ type GrowthStage = { 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 & { +type IrrigationRecommendationView = IrrigationRecommendationResult & { generated_at: string; recommendation_title: string; recommendation_subtitle: string; @@ -105,31 +87,14 @@ type MockIrrigationRecommendation = IrrigationRecommendationResult & { 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; + timeline: IrrigationRecommendationTimelineItem[]; + sections: IrrigationRecommendationSection[]; + alternative_plans: Array<{ + code: string; + title: string; + irrigation_method: string; + description: string; + }>; }; type DetailsSheetState = { @@ -139,27 +104,47 @@ type DetailsSheetState = { type: "balance" | "alternative" | "section"; }; -const formatStageLabel = (stage: string) => - GROWTH_STAGE_LABELS[stage] ?? +type InstructionStatItem = { + icon: string; + label: string; + value: string; + hint: string; +}; + +type GaugeLabels = { + currentSoilMoisture: string; + target: string; +}; + +const formatStageLabel = ( + stage: string, + getLabel?: (stage: string) => string | undefined, +) => + getLabel?.(stage) ?? stage .split(/[_-]/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); -const getPlantIcon = (icon: string) => PLANT_ICON_MAP[icon] ?? "tabler-leaf"; +const getPlantIcon = (icon: string) => { + if (!icon) return "tabler-leaf"; + if (icon.startsWith("tabler-")) return icon; + + return PLANT_ICON_MAP[icon] ?? "tabler-leaf"; +}; const getGrowthStageIcon = (stage: string) => GROWTH_STAGE_ICON_MAP[stage] ?? "tabler-circle-dot"; -const formatNumber = (value: number) => - new Intl.NumberFormat("fa-IR", { maximumFractionDigits: 2 }).format(value); +const formatNumber = (value: number, locale = "fa-IR") => + new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value); -const formatDateTime = (value: string) => { +const formatDateTime = (value: string, locale = "fa-IR") => { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; - return new Intl.DateTimeFormat("fa-IR", { + return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", @@ -177,200 +162,120 @@ const getStatusChipColor = ( 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 getErrorMessage = (error: unknown, fallback: string) => + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ? error.message + : fallback; -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 adaptRecommendation = ( + result: IrrigationRecommendationResult, + t: (key: string, values?: Record) => string, + formatPlant: (plant?: string | null, fallback?: string | null) => string, + formatStage: (stage: string) => string, + locale: string, +): IrrigationRecommendationView => { + const daily = result.water_balance?.daily ?? []; + const primaryDaily = daily[0]; + const durationMinutes = + typeof result.plan.durationMinutes === "number" + ? result.plan.durationMinutes + : Number(result.plan.durationMinutes ?? 0); + const frequencyPerWeek = + typeof result.plan.frequencyPerWeek === "number" + ? result.plan.frequencyPerWeek + : Number(result.plan.frequencyPerWeek ?? 0); + const moistureLevel = + typeof result.plan.moistureLevel === "number" + ? result.plan.moistureLevel + : Number(result.plan.moistureLevel ?? 0); + const sections = result.sections ?? []; -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("خیار", "باردهی"), - }, -]; + return { + ...result, + generated_at: new Date().toISOString(), + recommendation_title: t("recommendation.generatedTitle", { + plant: formatPlant(result.crop_id, result.plant_name), + }), + recommendation_subtitle: t("recommendation.generatedSubtitle", { + stage: formatStage(result.growth_stage ?? "-"), + }), + primary_method: { + label: result.irrigation_method_name ?? "-", + pressure: "", + target_moisture: Number.isFinite(moistureLevel) ? moistureLevel : 0, + coverage: "", + }, + usage_summary: { + duration_label: + durationMinutes > 0 + ? t("recommendation.durationPerTurn", { + value: formatNumber(durationMinutes, locale), + }) + : "-", + frequency_label: + frequencyPerWeek > 0 + ? t("recommendation.frequencyPerWeek", { + value: formatNumber(frequencyPerWeek, locale), + }) + : "-", + preferred_window: + result.plan.bestTimeOfDay ?? + primaryDaily?.irrigation_timing ?? + t("recommendation.weatherBasedWindow"), + area_basis_mm: Number(primaryDaily?.gross_irrigation_mm ?? 0), + area_basis_liters: Number(primaryDaily?.gross_irrigation_mm ?? 0) * 10000, + }, + timeline: result.timeline ?? [], + sections, + alternative_plans: [], + }; +}; export default function SmartIrrigationRecommendation() { const t = useTranslations("irrigation"); + const locale = useLocale(); const theme = useTheme(); + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid; 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 intlLocale = locale === "fa" ? "fa-IR" : locale; + + const getStageLabel = useCallback( + (stage: string) => { + const normalizedStage = stage.replace(/-/g, "_"); + const translationKey = `growthStage.options.${normalizedStage}`; + + return t.has(translationKey) ? t(translationKey) : undefined; + }, + [t], + ); + + const getHistoryStatusLabel = useCallback( + (status: string, fallback?: string | null) => { + const translationKey = `history.statuses.${status}`; + + return t.has(translationKey) ? t(translationKey) : fallback ?? status; + }, + [t], + ); + + const getCropLabel = useCallback( + (cropId?: string | null, fallback?: string | null) => { + if (!cropId) return fallback ?? "-"; + + const normalizedCropId = cropId.toLowerCase().replace(/[\s-]+/g, "_"); + const translationKey = `crops.${normalizedCropId}`; + + return t.has(translationKey) ? t(translationKey) : fallback ?? cropId; + }, + [t], + ); const [growthStages, setGrowthStages] = useState([]); const [cropOptions, setCropOptions] = useState([]); @@ -379,25 +284,25 @@ export default function SmartIrrigationRecommendation() { const [growthStage, setGrowthStage] = useState(""); const [selectedCrop, setSelectedCrop] = useState(null); const [recommendation, setRecommendation] = - useState(null); + 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 [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 [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({ @@ -407,89 +312,134 @@ export default function SmartIrrigationRecommendation() { type: "section", }); + const loadHistory = useCallback(async () => { + if (!farmUuid) { + setHistoryItems([]); + setHistoryError(null); + return; + } + + setHistoryLoading(true); + setHistoryError(null); + + try { + const response = await irrigationRecommendationService.getRecommendationsHistory( + farmUuid, + historyPage + 1, + historyPageSize, + ); + + setHistoryItems(response.data); + setHistoryPagination(response.pagination); + } catch (error) { + setHistoryError( + getErrorMessage(error, t("errors.historyFailed")), + ); + } finally { + setHistoryLoading(false); + } + }, [farmUuid, historyPage, historyPageSize, t]); + useEffect(() => { setRecommendation(null); setRequestError(null); setSelectedCrop(null); setGrowthStages([]); setGrowthStage(""); + if (!farmUuid) { + setCropOptions([]); + setConfigError(t("errors.noFarm")); + setConfigLoading(false); + return; + } + 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, + })); - const timer = window.setTimeout(() => { - setCropOptions(MOCK_CROP_OPTIONS); + setCropOptions(crops); - 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), - })) ?? []; + if (!crops.length) { + setConfigError(t("errors.noSelectedPlants")); + return; + } - setGrowthStages(stages); - setGrowthStage(stages[0]?.id ?? ""); - } + const firstCrop = crops[0]; - setConfigLoading(false); - }, 300); + if (firstCrop) { + setSelectedCrop(firstCrop.id); + const stages = + firstCrop.growthStages?.map((stage) => ({ + id: stage, + icon: getGrowthStageIcon(stage), + label: formatStageLabel(stage, getStageLabel), + })) ?? []; - return () => window.clearTimeout(timer); - }, []); + setGrowthStages(stages); + setGrowthStage(stages[0]?.id ?? ""); + } + }) + .catch((error) => { + setCropOptions([]); + setConfigError( + getErrorMessage(error, t("errors.selectedPlantsFailed")), + ); + }) + .finally(() => setConfigLoading(false)); + }, [farmUuid, getStageLabel, t]); useEffect(() => { - setHistoryLoading(true); - setHistoryError(null); + loadHistory(); + }, [loadHistory]); - 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 || !growthStage || !farmUuid) return; setLoading(true); setArea(1); setRecommendation(null); setRequestError(null); setStatusMessage(t("generating")); - setReasoningExpanded(false); - window.setTimeout(() => { + try { + const response = await irrigationRecommendationService.recommend({ + farm_uuid: farmUuid, + plant_name: selectedCrop, + growth_stage: growthStage, + }); + + if ("task_id" in response) { + setRequestError(t("errors.pendingResponse")); + setRecommendation(null); + void loadHistory(); + return; + } + setRecommendation( - buildMockRecommendation( - crop?.name ?? selectedCrop, - stage?.label ?? formatStageLabel(growthStage), + adaptRecommendation( + response, + t, + getCropLabel, + (stage) => formatStageLabel(stage, getStageLabel), + intlLocale, ), ); + void loadHistory(); + } catch (error) { + setRecommendation(null); + setRequestError(getErrorMessage(error, t("errors.generateFailed"))); + } finally { setLoading(false); setStatusMessage(null); - }, 700); + } }; const stageIndex = growthStages.findIndex((stage) => stage.id === growthStage); @@ -497,23 +447,60 @@ export default function SmartIrrigationRecommendation() { 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 resultContext = `${getCropLabel( + recommendation?.crop_id ?? selectedCrop, + recommendation?.plant_name ?? selectedCropOption?.name, + )} | ${ + recommendation?.growth_stage + ? formatStageLabel(recommendation.growth_stage, getStageLabel) + : selectedGrowthStage?.label ?? formatStageLabel(growthStage, getStageLabel) }`; 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 currentMoisture = + typeof recommendation?.plan.moistureLevel === "number" + ? recommendation.plan.moistureLevel + : Number(recommendation?.plan.moistureLevel ?? 0); + const instructionStats: InstructionStatItem[] = [ + { + icon: "tabler-droplet", + label: t("instructionStats.waterAmount.label"), + value: `${formatNumber(totalWaterLiters, intlLocale)} ${t("units.literShort")}`, + hint: `${formatNumber(totalWaterMm, intlLocale)} ${t("units.mm")} ${t("instructionStats.waterAmount.hint")}`, + }, + { + icon: "tabler-clock-hour-4", + label: t("instructionStats.duration.label"), + value: usageSummary?.duration_label ?? "-", + hint: t("instructionStats.duration.hint"), + }, + { + icon: "tabler-repeat", + label: t("instructionStats.frequency.label"), + value: usageSummary?.frequency_label ?? "-", + hint: t("instructionStats.frequency.hint"), + }, + { + icon: "tabler-sunrise", + label: t("instructionStats.bestWindow.label"), + value: usageSummary?.preferred_window ?? "-", + hint: t("instructionStats.bestWindow.hint"), + }, + ]; + + const gaugeLabels: GaugeLabels = { + currentSoilMoisture: t("gauge.currentSoilMoisture"), + target: t("gauge.target"), + }; const warningMessages = useMemo(() => { const items = [ @@ -532,12 +519,13 @@ export default function SmartIrrigationRecommendation() { ? crop.growthStages.map((stage) => ({ id: stage, icon: getGrowthStageIcon(stage), - label: formatStageLabel(stage), + label: formatStageLabel(stage, getStageLabel), })) : []; setGrowthStages(nextStages); setGrowthStage(nextStages[0]?.id ?? ""); + setRequestError(null); setRecommendation(null); return nextCrop; @@ -547,46 +535,25 @@ export default function SmartIrrigationRecommendation() { 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}`, + title: t("details.waterBalanceTitle", { date: item.forecast_date }), + content: t("details.waterBalanceContent", { + et0: formatNumber(item.et0_mm, intlLocale), + etc: formatNumber(item.etc_mm, intlLocale), + rainfall: formatNumber(item.effective_rainfall_mm, intlLocale), + gross: formatNumber(item.gross_irrigation_mm, intlLocale), + timing: item.irrigation_timing, + mm: t("units.mm"), + }), type: "balance", }); }; - const openAlternativeDetails = (item: AlternativePlan) => { - setDetailsSheet({ - isOpen: true, - title: item.title, - content: item.description, - type: "alternative", - }); - }; - - const openSectionDetails = (item: RecommendationSection) => { + const openSectionDetails = (item: IrrigationRecommendationSection) => { setDetailsSheet({ isOpen: true, title: item.title, @@ -599,27 +566,35 @@ export default function SmartIrrigationRecommendation() { setDetailsSheet((prev) => ({ ...prev, isOpen: false })); }; - const handleViewRecommendationReport = (recommendationUuid: string) => { - const historyItem = MOCK_HISTORY_SOURCE.find( - (item) => item.recommendation_uuid === recommendationUuid, - ); - - if (!historyItem) { - setRequestError("گزارش مورد نظر در داده های ماک پیدا نشد."); - return; - } - + const handleViewRecommendationReport = async (recommendationUuid: string) => { setLoading(true); setRequestError(null); - setStatusMessage("در حال دریافت گزارش توصیه"); + setStatusMessage(t("status.fetchingReport")); - window.setTimeout(() => { - setRecommendation(historyItem.result); - setReasoningExpanded(false); + try { + const response = + await irrigationRecommendationService.getRecommendationDetail( + recommendationUuid, + ); + + setRecommendation( + adaptRecommendation( + response, + t, + getCropLabel, + (stage) => formatStageLabel(stage, getStageLabel), + intlLocale, + ), + ); setArea(1); + } catch (error) { + setRequestError( + getErrorMessage(error, t("errors.reportFailed")), + ); + } finally { setLoading(false); setStatusMessage(null); - }, 400); + } }; return ( @@ -663,135 +638,119 @@ export default function SmartIrrigationRecommendation() { - - - - - - - - {recommendation.recommendation_title} - - - {primaryMethod?.label} - - - {recommendation.recommendation_subtitle} - - - - - - - - - - - رطوبت هدف: %{formatNumber(primaryMethod?.target_moisture ?? 0)} + {recommendation && ( + + + + + + {t("recommendation.instructionDetailsTitle")} + + + {t("recommendation.instructionDetailsSubtitle")} + - - - - محاسبه آب مورد نیاز برای مساحت مزرعه - - - هکتار - - - - - 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)} میلی متر در هر هکتار - + + + + {instructionStats.map((item) => ( + + + + + + + + {item.label} + + + {item.value} + + + {item.hint} + + + + + ))} - - - + + + + {t("recommendation.soilMoistureDriverTitle")} + + + {t("recommendation.soilMoistureDriverSubtitle")} + + + + + + + + + + + )} + + - تراز آب و نیاز روزانه + {t("recommendation.waterBalanceTitle")} - {formatNumber(item.gross_irrigation_mm)} mm + {formatNumber(item.gross_irrigation_mm, intlLocale)} {t("units.mm")} - بازه اجرا: {item.irrigation_timing} + {t("recommendation.executionWindow")}: {item.irrigation_timing} ))} @@ -845,11 +804,11 @@ export default function SmartIrrigationRecommendation() { color="text.secondary" className="mb-3" > - پروفایل گیاه + {t("recommendation.cropProfileTitle")} - - - - - - - - - - {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} - - - - - - - - مراحل و دستورالعمل اجرا + {t("recommendation.executionStepsTitle")} {applicationSteps.map((item, index) => ( - + {item.title} 0 && ( - هشدارها و نکات مهم + {t("recommendation.warningsTitle")} {warningMessages.map((warning, index) => ( @@ -1064,83 +932,8 @@ export default function SmartIrrigationRecommendation() { )} - - - برنامه های جایگزین - - - اگر شرایط اقلیمی یا برنامه بهره برداری تغییر کرد، می توانید از نسخه های جایگزین زیر استفاده کنید. - - - - {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.length && ( + {tipSections.map((item) => ( ))} - )} - + + )} - ذخیره این نسخه + {t("actions.saveVersion")} @@ -1236,11 +1029,29 @@ export default function SmartIrrigationRecommendation() { > {t("title")} - + {t("subtitle")} + {typeof farmHub?.irrigation_method_name === "string" && + farmHub.irrigation_method_name && ( + + {t("farmIrrigationMethod", { method: farmHub.irrigation_method_name })} + + )} + {!!growthStages.length && ( <> @@ -1324,7 +1135,7 @@ export default function SmartIrrigationRecommendation() { handleCropSelect(crop)} /> @@ -1386,7 +1197,7 @@ export default function SmartIrrigationRecommendation() { - {statusMessage ?? "در حال تحلیل و تولید نسخه آبیاری..."} + {statusMessage ?? t("loadingCard")} @@ -1395,10 +1206,10 @@ export default function SmartIrrigationRecommendation() { - تاریخچه توصیه های آبیاری + {t("history.title")} - همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید. + {t("history.subtitle")} @@ -1424,11 +1235,11 @@ export default function SmartIrrigationRecommendation() { - تاریخ ثبت - محصول / مرحله رشد - روش آبیاری - وضعیت - گزارش + {t("history.columns.requestedAt")} + {t("history.columns.cropStage")} + {t("history.columns.method")} + {t("history.columns.status")} + {t("history.columns.report")} @@ -1441,29 +1252,29 @@ export default function SmartIrrigationRecommendation() { ) : historyItems.length ? ( historyItems.map((item) => ( - {formatDateTime(item.requested_at)} + {formatDateTime(item.requested_at, intlLocale)} - {item.plant_name} + {getCropLabel(item.crop_id, item.plant_name)} - {formatStageLabel(item.growth_stage)} + {formatStageLabel(item.growth_stage ?? "-", getStageLabel)} - {item.irrigation_method} + {item.irrigation_method_name ?? "-"} - + handleViewRecommendationReport( @@ -1488,7 +1299,7 @@ export default function SmartIrrigationRecommendation() { - هنوز توصیه ای برای این مزرعه ثبت نشده است. + {t("history.empty")} @@ -1508,6 +1319,14 @@ export default function SmartIrrigationRecommendation() { setHistoryPageSize(Number(event.target.value)); }} rowsPerPageOptions={[5, 10, 20]} + labelRowsPerPage={t("history.rowsPerPage")} + labelDisplayedRows={({ from, to, count }) => + t("history.displayedRows", { + from, + to, + count: count === -1 ? `+${to}` : count, + }) + } /> @@ -1587,7 +1406,7 @@ export default function SmartIrrigationRecommendation() { }, }} > - {detailsSheet.type === "alternative" ? "افزودن به یادآور" : "متوجه شدم"} + {detailsSheet.type === "alternative" ? t("details.addReminder") : t("details.understood")} @@ -1657,33 +1476,78 @@ function CropCard({ ); } -function PrescriptionRow({ - icon, - label, + +function GaugeMeter({ value, + target, + accent, + labels, + locale, }: { - icon: string; - label: string; - value: string; + value: number; + target: number; + accent: string; + labels: GaugeLabels; + locale?: string; }) { const theme = useTheme(); - const primaryMain = theme.palette.primary.main; + const [animatedValue, setAnimatedValue] = useState(0); + const safeValue = Math.min(Math.max(value, 0), 100); + const radius = 62; + const circumference = 2 * Math.PI * radius; + const dashOffset = circumference - (animatedValue / 100) * circumference; + + useEffect(() => { + const frame = window.requestAnimationFrame(() => { + setAnimatedValue(safeValue); + }); + + return () => window.cancelAnimationFrame(frame); + }, [safeValue]); return ( - - - - - {label} + + + + + + + + + + {labels.currentSoilMoisture} - - {value} + + {formatNumber(safeValue, locale)}% + + + {labels.target}: {formatNumber(target, locale)}%