UPDATE
This commit is contained in:
@@ -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` هم تبدیل کنم.
|
||||||
@@ -4,11 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from "../client";
|
import { apiClient } from "../client";
|
||||||
import type {
|
|
||||||
RecommendationTaskInitResponse,
|
|
||||||
RecommendationTaskStatusResponse,
|
|
||||||
} from "./recommendationTask";
|
|
||||||
import { normalizeRecommendationTaskStatus } from "./recommendationTask";
|
|
||||||
|
|
||||||
const PREFIX = "/api/fertilization-recommendation";
|
const PREFIX = "/api/fertilization-recommendation";
|
||||||
const RECOMMEND_PREFIX = "/api/fertilization";
|
const RECOMMEND_PREFIX = "/api/fertilization";
|
||||||
@@ -40,12 +35,95 @@ export interface FertilizationConfigResponse {
|
|||||||
cropOptions: CropOption[];
|
cropOptions: CropOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FertilizationPlan {
|
export interface FertilizationNpkRatio {
|
||||||
npkRatio: string;
|
n: number;
|
||||||
amountPerHectare: string;
|
p: number;
|
||||||
applicationMethod: string;
|
k: number;
|
||||||
applicationInterval: string;
|
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;
|
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 {
|
export interface FertilizationRecommendPayload {
|
||||||
@@ -58,17 +136,9 @@ export interface FertilizationRecommendPayload {
|
|||||||
waterEC?: string;
|
waterEC?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FertilizationRecommendationResult {
|
|
||||||
plan: FertilizationPlan;
|
|
||||||
status?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FertilizationRecommendResponse =
|
|
||||||
| FertilizationRecommendationResult
|
|
||||||
| RecommendationTaskInitResponse;
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
status: string;
|
code: number;
|
||||||
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,26 +147,6 @@ async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
|||||||
return res.data;
|
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 = {
|
export const fertilizationRecommendationService = {
|
||||||
getConfig(farmUuid: string): Promise<FertilizationConfigResponse> {
|
getConfig(farmUuid: string): Promise<FertilizationConfigResponse> {
|
||||||
return unwrap(
|
return unwrap(
|
||||||
@@ -108,39 +158,12 @@ export const fertilizationRecommendationService = {
|
|||||||
|
|
||||||
recommend(
|
recommend(
|
||||||
payload?: FertilizationRecommendPayload,
|
payload?: FertilizationRecommendPayload,
|
||||||
): Promise<FertilizationRecommendResponse> {
|
): Promise<FertilizationRecommendationResult> {
|
||||||
return unwrap(
|
return unwrap(
|
||||||
apiClient.post<ApiResponse<FertilizationRecommendResponse>>(
|
apiClient.post<ApiResponse<FertilizationRecommendationResult>>(
|
||||||
`${RECOMMEND_PREFIX}/recommend/`,
|
`${RECOMMEND_PREFIX}/recommend/`,
|
||||||
payload ?? {},
|
payload ?? {},
|
||||||
),
|
),
|
||||||
).then((response) =>
|
|
||||||
"task_id" in response
|
|
||||||
? normalizeTaskInitResponse(response)
|
|
||||||
: normalizeRecommendationResult(response),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getRecommendStatus(
|
|
||||||
taskId: string,
|
|
||||||
farmUuid: string,
|
|
||||||
): Promise<
|
|
||||||
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
|
|
||||||
> {
|
|
||||||
return unwrap(
|
|
||||||
apiClient.get<
|
|
||||||
ApiResponse<
|
|
||||||
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
|
|
||||||
>
|
|
||||||
>(
|
|
||||||
`${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
|
||||||
),
|
|
||||||
).then((response) => ({
|
|
||||||
...response,
|
|
||||||
status: normalizeRecommendationTaskStatus(response.status),
|
|
||||||
result: response.result
|
|
||||||
? normalizeRecommendationResult(response.result)
|
|
||||||
: undefined,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+592
-133
@@ -1,24 +1,36 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import CardContent from "@mui/material/CardContent";
|
import CardContent from "@mui/material/CardContent";
|
||||||
import Typography from "@mui/material/Typography";
|
import Chip from "@mui/material/Chip";
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Collapse from "@mui/material/Collapse";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
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 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 { useFarmHub } from "@/hooks/useFarmHub";
|
||||||
import type {
|
import {
|
||||||
GrowthStage,
|
fertilizationRecommendationService,
|
||||||
CropOption,
|
type CropOption,
|
||||||
FertilizationPlan,
|
type FertilizationAlternativeRecommendation,
|
||||||
|
type FertilizationNutrientItem,
|
||||||
|
type FertilizationRecommendationResult,
|
||||||
|
type GrowthStage,
|
||||||
} from "@/libs/api/services/fertilizationRecommendationService";
|
} 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";
|
import { selectedPlantsService } from "@/libs/api/services/selectedPlantsService";
|
||||||
|
|
||||||
const GROWTH_STAGE_LABELS: Record<string, string> = {
|
const GROWTH_STAGE_LABELS: Record<string, string> = {
|
||||||
@@ -36,6 +48,7 @@ const PLANT_ICON_MAP: Record<string, string> = {
|
|||||||
saffron: "tabler-flower-2",
|
saffron: "tabler-flower-2",
|
||||||
canola: "tabler-leaf",
|
canola: "tabler-leaf",
|
||||||
vegetables: "tabler-carrot",
|
vegetables: "tabler-carrot",
|
||||||
|
cucumber: "tabler-leaf",
|
||||||
};
|
};
|
||||||
|
|
||||||
const GROWTH_STAGE_ICON_MAP: Record<string, string> = {
|
const GROWTH_STAGE_ICON_MAP: Record<string, string> = {
|
||||||
@@ -51,14 +64,13 @@ const formatStageLabel = (stage: string) =>
|
|||||||
stage
|
stage
|
||||||
.split(/[_-]/)
|
.split(/[_-]/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const getPlantIcon = (icon: string) => PLANT_ICON_MAP[icon] ?? "tabler-leaf";
|
const getPlantIcon = (icon: string) => PLANT_ICON_MAP[icon] ?? "tabler-leaf";
|
||||||
const getGrowthStageIcon = (stage: string) =>
|
const getGrowthStageIcon = (stage: string) =>
|
||||||
GROWTH_STAGE_ICON_MAP[stage] ?? "tabler-circle-dot";
|
GROWTH_STAGE_ICON_MAP[stage] ?? "tabler-circle-dot";
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
const getErrorMessage = (error: unknown, fallback: string) =>
|
const getErrorMessage = (error: unknown, fallback: string) =>
|
||||||
typeof error === "object" &&
|
typeof error === "object" &&
|
||||||
error !== null &&
|
error !== null &&
|
||||||
@@ -67,6 +79,20 @@ const getErrorMessage = (error: unknown, fallback: string) =>
|
|||||||
? error.message
|
? error.message
|
||||||
: fallback;
|
: 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() {
|
export default function SmartFertilizationRecommendation() {
|
||||||
const t = useTranslations("fertilization");
|
const t = useTranslations("fertilization");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -76,20 +102,29 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
const primaryLight = theme.palette.primary.light;
|
const primaryLight = theme.palette.primary.light;
|
||||||
const primaryDark = theme.palette.primary.dark;
|
const primaryDark = theme.palette.primary.dark;
|
||||||
const paperBg = theme.palette.background.paper;
|
const paperBg = theme.palette.background.paper;
|
||||||
|
|
||||||
const [growthStages, setGrowthStages] = useState<GrowthStage[]>([]);
|
const [growthStages, setGrowthStages] = useState<GrowthStage[]>([]);
|
||||||
const [cropOptions, setCropOptions] = useState<CropOption[]>([]);
|
const [cropOptions, setCropOptions] = useState<CropOption[]>([]);
|
||||||
const [configLoading, setConfigLoading] = useState(true);
|
const [configLoading, setConfigLoading] = useState(true);
|
||||||
const [configError, setConfigError] = useState<string | null>(null);
|
const [configError, setConfigError] = useState<string | null>(null);
|
||||||
const [growthStage, setGrowthStage] = useState<string>("");
|
const [growthStage, setGrowthStage] = useState("");
|
||||||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null);
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null);
|
||||||
const [plan, setPlan] = useState<FertilizationPlan | null>(null);
|
const [recommendation, setRecommendation] =
|
||||||
|
useState<FertilizationRecommendationResult | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [requestError, setRequestError] = useState<string | null>(null);
|
const [requestError, setRequestError] = useState<string | null>(null);
|
||||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||||
const [reasoningExpanded, setReasoningExpanded] = useState(false);
|
const [reasoningExpanded, setReasoningExpanded] = useState(false);
|
||||||
|
const [area, setArea] = useState(1);
|
||||||
|
const [detailsSheet, setDetailsSheet] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPlan(null);
|
setRecommendation(null);
|
||||||
setRequestError(null);
|
setRequestError(null);
|
||||||
setSelectedCrop(null);
|
setSelectedCrop(null);
|
||||||
setGrowthStages([]);
|
setGrowthStages([]);
|
||||||
@@ -123,75 +158,38 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
id: stage,
|
id: stage,
|
||||||
icon: getGrowthStageIcon(stage),
|
icon: getGrowthStageIcon(stage),
|
||||||
label: formatStageLabel(stage),
|
label: formatStageLabel(stage),
|
||||||
})) ??
|
})) ?? [];
|
||||||
[];
|
|
||||||
|
|
||||||
setGrowthStages(stages);
|
setGrowthStages(stages);
|
||||||
setGrowthStage(stages[0]?.id ?? "");
|
setGrowthStage(stages[0]?.id ?? "");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: { message?: string }) => {
|
.catch((error: { message?: string }) => {
|
||||||
setConfigError(err?.message ?? "Failed to load plants");
|
setConfigError(error?.message ?? "Failed to load plants");
|
||||||
})
|
})
|
||||||
.finally(() => setConfigLoading(false));
|
.finally(() => setConfigLoading(false));
|
||||||
}, [farmUuid, t]);
|
}, [farmUuid, t]);
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!selectedCrop || !farmUuid) return;
|
if (!selectedCrop || !growthStage || !farmUuid) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setPlan(null);
|
setArea(1);
|
||||||
|
setRecommendation(null);
|
||||||
setRequestError(null);
|
setRequestError(null);
|
||||||
setStatusMessage(t("generating"));
|
setStatusMessage(t("generating"));
|
||||||
setReasoningExpanded(false);
|
setReasoningExpanded(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const recommendation = await fertilizationRecommendationService.recommend(
|
const response = await fertilizationRecommendationService.recommend({
|
||||||
{
|
|
||||||
farm_uuid: farmUuid,
|
farm_uuid: farmUuid,
|
||||||
crop_id: selectedCrop,
|
crop_id: selectedCrop,
|
||||||
growth_stage: growthStage,
|
growth_stage: growthStage,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if ("task_id" in recommendation) {
|
setRecommendation(response);
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setPlan(null);
|
setRecommendation(null);
|
||||||
setRequestError(getErrorMessage(error, t("errors.generateFailed")));
|
setRequestError(getErrorMessage(error, t("errors.generateFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 =
|
const selectedCropOption =
|
||||||
cropOptions.find((option) => option.id === selectedCrop) ?? null;
|
cropOptions.find((option) => option.id === selectedCrop) ?? null;
|
||||||
const selectedGrowthStage =
|
const selectedGrowthStage =
|
||||||
@@ -208,6 +206,35 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
selectedGrowthStage?.label ?? formatStageLabel(growthStage)
|
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) => {
|
const handleCropSelect = (crop: CropOption) => {
|
||||||
setSelectedCrop((prev) => {
|
setSelectedCrop((prev) => {
|
||||||
const nextCrop = prev === crop.id ? null : crop.id;
|
const nextCrop = prev === crop.id ? null : crop.id;
|
||||||
@@ -222,16 +249,52 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
|
|
||||||
setGrowthStages(nextStages);
|
setGrowthStages(nextStages);
|
||||||
setGrowthStage(nextStages[0]?.id ?? "");
|
setGrowthStage(nextStages[0]?.id ?? "");
|
||||||
|
setRecommendation(null);
|
||||||
|
|
||||||
return nextCrop;
|
return nextCrop;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToForm = () => {
|
const handleBackToForm = () => {
|
||||||
setPlan(null);
|
setRecommendation(null);
|
||||||
|
setArea(1);
|
||||||
setReasoningExpanded(false);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="min-bs-screen"
|
className="min-bs-screen"
|
||||||
@@ -241,10 +304,8 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box className={`mx-auto px-4 ${recommendation ? "py-0 sm:py-0" : "py-6 sm:py-8"}`}>
|
||||||
className={`max-w-lg mx-auto px-4 ${plan ? "py-0 sm:py-0" : "py-6 sm:py-8"}`}
|
{recommendation ? (
|
||||||
>
|
|
||||||
{plan ? (
|
|
||||||
<Box className="animate-fade-in">
|
<Box className="animate-fade-in">
|
||||||
<Box
|
<Box
|
||||||
position="sticky"
|
position="sticky"
|
||||||
@@ -266,10 +327,7 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
borderRadius: "16px",
|
borderRadius: "16px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i
|
<i className="tabler-arrow-right text-xl" style={{ color: primaryMain }} />
|
||||||
className="tabler-arrow-right text-xl"
|
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="subtitle1" fontWeight="bold">
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
{resultContext}
|
{resultContext}
|
||||||
@@ -277,7 +335,231 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="pb-28">
|
<Box className="space-y-5 pb-28">
|
||||||
|
<Box className="grid grid-cols-12 gap-5 items-start">
|
||||||
|
<Box className="col-span-12 lg:col-span-6">
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: "32px",
|
||||||
|
background: `linear-gradient(145deg, ${alpha(primaryLight, 0.42)} 0%, ${alpha(primaryMain, 0.2)} 55%, ${paperBg} 100%)`,
|
||||||
|
boxShadow: `0 12px 40px ${alpha(primaryMain, 0.16)}, 0 4px 18px ${alpha(primaryMain, 0.08)}`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.16)}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<Box className="flex items-start justify-between gap-3">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="overline" color="primary.main" fontWeight={700}>
|
||||||
|
{displayTitle}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" fontWeight={800} color="text.primary">
|
||||||
|
{fertilizerName}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
fontWeight={700}
|
||||||
|
sx={{ color: primaryDark }}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
NPK: {primaryRecommendation?.npk_ratio.label}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl"
|
||||||
|
sx={{ backgroundColor: alpha(primaryMain, 0.12) }}
|
||||||
|
>
|
||||||
|
<i className="tabler-atom-2 text-2xl" style={{ color: primaryMain }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="mt-4 flex flex-wrap items-center gap-3">
|
||||||
|
<Chip
|
||||||
|
label={`روش مصرف: ${primaryRecommendation?.application_method.label ?? "-"}`}
|
||||||
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: alpha(primaryMain, 0.12),
|
||||||
|
color: primaryDark,
|
||||||
|
fontWeight: 700,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.14)}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="body1" fontWeight={600} color="text.secondary">
|
||||||
|
مقدار پایه: {primaryRecommendation?.dosage.label ?? "-"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="mt-5 rounded-[24px] p-4"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: alpha(theme.palette.background.paper, 0.72),
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="flex items-center justify-between gap-3">
|
||||||
|
<Typography variant="subtitle1" fontWeight={700} color="text.primary">
|
||||||
|
محاسبه مقدار برای مساحت مزرعه
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
هکتار
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-[minmax(0,1fr)_120px] sm:items-center">
|
||||||
|
<Slider
|
||||||
|
value={area}
|
||||||
|
min={0.5}
|
||||||
|
max={100}
|
||||||
|
step={0.5}
|
||||||
|
onChange={(_, value) => setArea(value as number)}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
sx={{
|
||||||
|
color: primaryMain,
|
||||||
|
"& .MuiSlider-valueLabel": {
|
||||||
|
backgroundColor: primaryDark,
|
||||||
|
borderRadius: "10px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={area}
|
||||||
|
onChange={(event) => 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),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="mt-5 rounded-[20px] p-4"
|
||||||
|
sx={{ backgroundColor: alpha(primaryMain, 0.08) }}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
نیاز کل مزرعه شما:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" fontWeight={800} sx={{ color: primaryMain }}>
|
||||||
|
{formatNumber(totalAmount)} {dosageUnit}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||||
|
بر پایه {formatNumber(baseAmountPerHectare)} {dosageUnit} در هر هکتار
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||||
|
معادل {formatNumber(baseAmountPerSquareMeter)} {dosageUnit} در هر متر مربع
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="col-span-12 lg:col-span-6">
|
||||||
|
<Box className="mt-2 lg:mt-0">
|
||||||
|
<Typography variant="h6" fontWeight={700} className="mb-4 mt-2 lg:mt-0">
|
||||||
|
آنالیز ترکیبات
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
className="rounded-[24px] border p-4"
|
||||||
|
sx={{
|
||||||
|
borderColor: alpha(primaryMain, 0.14),
|
||||||
|
background: `linear-gradient(180deg, ${alpha(theme.palette.background.paper, 0.96)} 0%, ${alpha(primaryMain, 0.04)} 100%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col gap-y-4">
|
||||||
|
{macroNutrients.map((item) => (
|
||||||
|
<Box
|
||||||
|
key={item.key}
|
||||||
|
className="space-y-2 cursor-pointer rounded-2xl px-2 py-2 transition-colors"
|
||||||
|
onClick={() => openNutrientDetails(item)}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: alpha(primaryMain, 0.04),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="flex items-center justify-between gap-3">
|
||||||
|
<Typography variant="body2" fontWeight={600} color="text.primary">
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={700} color="text.secondary">
|
||||||
|
{formatNumber(item.value)}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={Math.min(item.value, 100)}
|
||||||
|
color={
|
||||||
|
item.key === "p"
|
||||||
|
? "secondary"
|
||||||
|
: item.key === "k"
|
||||||
|
? "success"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
height: 10,
|
||||||
|
borderRadius: "999px",
|
||||||
|
backgroundColor: alpha(primaryMain, 0.08),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="mt-5">
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
fontWeight={700}
|
||||||
|
color="text.secondary"
|
||||||
|
className="mb-3"
|
||||||
|
>
|
||||||
|
ریزمغذی ها
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex flex-wrap gap-2">
|
||||||
|
{microNutrients.length ? (
|
||||||
|
microNutrients.map((item) => (
|
||||||
|
<Chip
|
||||||
|
key={item.key}
|
||||||
|
label={`${item.name}: ${formatNumber(item.value)}٪`}
|
||||||
|
variant="outlined"
|
||||||
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
borderColor: alpha(primaryMain, 0.16),
|
||||||
|
color: "text.secondary",
|
||||||
|
backgroundColor: alpha(primaryMain, 0.03),
|
||||||
|
}}
|
||||||
|
onClick={() => openNutrientDetails(item)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
label="ریزمغذی مشخصی در نسخه ثبت نشده است"
|
||||||
|
variant="outlined"
|
||||||
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
borderColor: alpha(primaryMain, 0.16),
|
||||||
|
color: "text.secondary",
|
||||||
|
backgroundColor: alpha(primaryMain, 0.03),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="grid grid-cols-12 gap-5 items-start">
|
||||||
|
<Box className="col-span-12 lg:col-span-6">
|
||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -290,10 +572,7 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
>
|
>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<Box className="flex items-center gap-2 mbe-5">
|
<Box className="flex items-center gap-2 mbe-5">
|
||||||
<i
|
<i className="tabler-prescription text-2xl" style={{ color: primaryMain }} />
|
||||||
className="tabler-prescription text-2xl"
|
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
<Typography variant="h6" fontWeight={700} color="text.primary">
|
<Typography variant="h6" fontWeight={700} color="text.primary">
|
||||||
{t("result.title")}
|
{t("result.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -303,25 +582,35 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
<PrescriptionRow
|
<PrescriptionRow
|
||||||
icon="tabler-atom-2"
|
icon="tabler-atom-2"
|
||||||
label={t("result.fertilizerType")}
|
label={t("result.fertilizerType")}
|
||||||
value={plan.npkRatio}
|
value={primaryRecommendation?.npk_ratio.label ?? "-"}
|
||||||
/>
|
/>
|
||||||
<PrescriptionRow
|
<PrescriptionRow
|
||||||
icon="tabler-scale"
|
icon="tabler-scale"
|
||||||
label={t("result.amountPerHectare")}
|
label={t("result.amountPerHectare")}
|
||||||
value={plan.amountPerHectare}
|
value={primaryRecommendation?.dosage.label ?? "-"}
|
||||||
/>
|
/>
|
||||||
<PrescriptionRow
|
<PrescriptionRow
|
||||||
icon="tabler-spray"
|
icon="tabler-spray"
|
||||||
label={t("result.applicationMethod")}
|
label={t("result.applicationMethod")}
|
||||||
value={plan.applicationMethod}
|
value={primaryRecommendation?.application_method.label ?? "-"}
|
||||||
/>
|
/>
|
||||||
<PrescriptionRow
|
<PrescriptionRow
|
||||||
icon="tabler-calendar-repeat"
|
icon="tabler-calendar-repeat"
|
||||||
label={t("result.applicationInterval")}
|
label={t("result.applicationInterval")}
|
||||||
value={plan.applicationInterval}
|
value={primaryRecommendation?.application_interval.label ?? "-"}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{recommendationSection?.validityPeriod && (
|
||||||
|
<Box className="mt-3">
|
||||||
|
<PrescriptionRow
|
||||||
|
icon="tabler-clock-hour-4"
|
||||||
|
label="بازه اعتبار"
|
||||||
|
value={recommendationSection.validityPeriod}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
className="mt-5 rounded-2xl overflow-hidden transition-all duration-300"
|
className="mt-5 rounded-2xl overflow-hidden transition-all duration-300"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -337,15 +626,8 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
sx={{ "&:hover": { bgcolor: alpha(primaryMain, 0.06) } }}
|
sx={{ "&:hover": { bgcolor: alpha(primaryMain, 0.06) } }}
|
||||||
>
|
>
|
||||||
<Box className="flex items-center gap-2">
|
<Box className="flex items-center gap-2">
|
||||||
<i
|
<i className="tabler-brain text-lg" style={{ color: primaryMain }} />
|
||||||
className="tabler-brain text-lg"
|
<Typography variant="subtitle2" fontWeight={600} color="text.primary">
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
fontWeight={600}
|
|
||||||
color="text.primary"
|
|
||||||
>
|
|
||||||
{t("result.whyRecommendation")}
|
{t("result.whyRecommendation")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -358,12 +640,8 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Collapse in={reasoningExpanded}>
|
<Collapse in={reasoningExpanded}>
|
||||||
<Box className="px-4 pb-4">
|
<Box className="px-4 pb-4">
|
||||||
<Typography
|
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.7 }}>
|
||||||
variant="body2"
|
{primaryRecommendation?.reasoning}
|
||||||
color="text.secondary"
|
|
||||||
sx={{ lineHeight: 1.7 }}
|
|
||||||
>
|
|
||||||
{plan.reasoning}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@@ -372,6 +650,168 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box className="col-span-12 lg:col-span-6">
|
||||||
|
<Box className="mt-6 lg:mt-0">
|
||||||
|
<Typography variant="h6" fontWeight={700} className="mb-4 lg:mt-0">
|
||||||
|
مراحل و دستورالعمل مصرف
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
className="rounded-[24px] border p-4"
|
||||||
|
sx={{
|
||||||
|
borderColor: alpha(primaryMain, 0.14),
|
||||||
|
background: `linear-gradient(180deg, ${alpha(theme.palette.background.paper, 0.98)} 0%, ${alpha(primaryMain, 0.03)} 100%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stepper
|
||||||
|
orientation="vertical"
|
||||||
|
activeStep={applicationSteps.length}
|
||||||
|
sx={{
|
||||||
|
"& .MuiStepConnector-line": {
|
||||||
|
borderColor: alpha(primaryMain, 0.18),
|
||||||
|
minHeight: 24,
|
||||||
|
},
|
||||||
|
"& .MuiStepLabel-iconContainer": {
|
||||||
|
paddingInlineEnd: 1.5,
|
||||||
|
},
|
||||||
|
"& .MuiStepLabel-label": {
|
||||||
|
fontWeight: 700,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .MuiStepIcon-root": {
|
||||||
|
color: alpha(primaryMain, 0.22),
|
||||||
|
},
|
||||||
|
"& .MuiStepIcon-root.Mui-active, & .MuiStepIcon-root.Mui-completed": {
|
||||||
|
color: primaryMain,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{applicationSteps.map((item, index) => (
|
||||||
|
<Step key={item.step_number} expanded active completed={index < applicationSteps.length}>
|
||||||
|
<StepLabel>{item.title}</StepLabel>
|
||||||
|
<StepContent>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
className="pb-2 text-sm leading-7"
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
</StepContent>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{warningMessages.length > 0 && (
|
||||||
|
<Box className="mt-6">
|
||||||
|
<Typography variant="h6" fontWeight={700} className="mb-4">
|
||||||
|
هشدارها و نکات مهم
|
||||||
|
</Typography>
|
||||||
|
<Box className="grid grid-cols-1 gap-3">
|
||||||
|
{warningMessages.map((warning, index) => (
|
||||||
|
<Alert
|
||||||
|
key={`${warning}-${index}`}
|
||||||
|
severity="warning"
|
||||||
|
className="rounded-[20px]"
|
||||||
|
sx={{
|
||||||
|
alignItems: "flex-start",
|
||||||
|
borderRadius: "20px",
|
||||||
|
border: `1px solid ${alpha(theme.palette.warning.main, 0.18)}`,
|
||||||
|
backgroundColor: alpha(theme.palette.warning.main, 0.1),
|
||||||
|
"& .MuiAlert-message": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{warning}
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box className="mt-8">
|
||||||
|
<Typography variant="h6" fontWeight={700}>
|
||||||
|
کودهای جایگزین
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1 mb-4">
|
||||||
|
در صورت عدم دسترسی به پیشنهاد اصلی، می توانید از موارد زیر استفاده کنید:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="flex gap-4 overflow-x-auto pb-4 snap-x snap-mandatory"
|
||||||
|
sx={{
|
||||||
|
scrollBehavior: "smooth",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
msOverflowStyle: "none",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alternativeFertilizers.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.fertilizer_code}
|
||||||
|
elevation={0}
|
||||||
|
className="min-w-[220px] snap-center rounded-[20px]"
|
||||||
|
onClick={() => 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full flex-col p-4">
|
||||||
|
<Box className="mb-4 flex items-center justify-between gap-3">
|
||||||
|
<Box
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-2xl"
|
||||||
|
sx={{ backgroundColor: alpha(primaryMain, 0.1) }}
|
||||||
|
>
|
||||||
|
<i className="tabler-flask text-xl" style={{ color: primaryMain }} />
|
||||||
|
</Box>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={item.fertilizer_type}
|
||||||
|
className="rounded-xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: alpha(primaryMain, 0.1),
|
||||||
|
color: primaryDark,
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" color="text.primary">
|
||||||
|
{item.fertilizer_name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="caption" color="text.secondary" className="mt-2 block text-xs">
|
||||||
|
روش مصرف: {item.usage_method}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
className="mt-4 self-start rounded-xl"
|
||||||
|
sx={{
|
||||||
|
borderColor: alpha(primaryMain, 0.22),
|
||||||
|
color: primaryMain,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
انتخاب این مورد
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
position="fixed"
|
position="fixed"
|
||||||
bottom={0}
|
bottom={0}
|
||||||
@@ -431,23 +871,14 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
>
|
>
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body2" color="text.secondary" className="mt-1 transition-colors duration-300">
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
className="mt-1 transition-colors duration-300"
|
|
||||||
>
|
|
||||||
{t("subtitle")}
|
{t("subtitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{!!growthStages.length && (
|
{!!growthStages.length && (
|
||||||
<>
|
<>
|
||||||
<Typography
|
<Typography variant="subtitle2" fontWeight={600} color="text.secondary" className="mbe-3">
|
||||||
variant="subtitle2"
|
|
||||||
fontWeight={600}
|
|
||||||
color="text.secondary"
|
|
||||||
className="mbe-3"
|
|
||||||
>
|
|
||||||
{t("growthStage.title")}
|
{t("growthStage.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box className="flex gap-2 mb-6 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-hide">
|
<Box className="flex gap-2 mb-6 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-hide">
|
||||||
@@ -510,12 +941,7 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Typography
|
<Typography variant="subtitle2" fontWeight={600} color="text.secondary" className="mbe-3">
|
||||||
variant="subtitle2"
|
|
||||||
fontWeight={600}
|
|
||||||
color="text.secondary"
|
|
||||||
className="mbe-3"
|
|
||||||
>
|
|
||||||
{t("plantSelection.title")}
|
{t("plantSelection.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
{configLoading ? (
|
{configLoading ? (
|
||||||
@@ -592,10 +1018,7 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
background: `linear-gradient(135deg, ${alpha(primaryMain, 0.15)} 0%, ${alpha(primaryMain, 0.08)} 100%)`,
|
background: `linear-gradient(135deg, ${alpha(primaryMain, 0.15)} 0%, ${alpha(primaryMain, 0.08)} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i
|
<i className="tabler-sparkles text-2xl" style={{ color: primaryMain }} />
|
||||||
className="tabler-sparkles text-2xl"
|
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{statusMessage ?? t("generating")}
|
{statusMessage ?? t("generating")}
|
||||||
@@ -604,6 +1027,50 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
open={detailsSheet.isOpen}
|
||||||
|
onClose={closeDetailsSheet}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
p: 2,
|
||||||
|
maxHeight: "80vh",
|
||||||
|
zIndex: 1400,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="w-10 h-1.5 bg-gray-300 rounded-full mx-auto mb-4" />
|
||||||
|
<Box className="flex items-center justify-between gap-3">
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{detailsSheet.title}
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={closeDetailsSheet}>
|
||||||
|
<i className="tabler-x text-xl" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ mt: 2, color: "text.secondary", lineHeight: 1.8 }}>
|
||||||
|
{detailsSheet.content}
|
||||||
|
</Typography>
|
||||||
|
<Box className="mt-6">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
onClick={closeDetailsSheet}
|
||||||
|
sx={{
|
||||||
|
borderRadius: "16px",
|
||||||
|
backgroundColor: detailsSheet.type === "alternative" ? primaryDark : primaryMain,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: detailsSheet.type === "alternative" ? primaryMain : primaryDark,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{detailsSheet.type === "alternative" ? "افزودن به یادآور" : "متوجه شدم"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -623,6 +1090,7 @@ function CropCard({
|
|||||||
const primaryMain = theme.palette.primary.main;
|
const primaryMain = theme.palette.primary.main;
|
||||||
const primaryDark = theme.palette.primary.dark;
|
const primaryDark = theme.palette.primary.dark;
|
||||||
const paperBg = theme.palette.background.paper;
|
const paperBg = theme.palette.background.paper;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
component="button"
|
component="button"
|
||||||
@@ -659,18 +1127,11 @@ function CropCard({
|
|||||||
style={!selected ? { color: primaryMain } : undefined}
|
style={!selected ? { color: primaryMain } : undefined}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography
|
<Typography variant="body2" fontWeight={600} color={selected ? "primary.main" : "text.primary"}>
|
||||||
variant="body2"
|
|
||||||
fontWeight={600}
|
|
||||||
color={selected ? "primary.main" : "text.primary"}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</Typography>
|
||||||
{selected && (
|
{selected && (
|
||||||
<i
|
<i className="tabler-circle-check-filled text-xl ms-auto" style={{ color: primaryMain }} />
|
||||||
className="tabler-circle-check-filled text-xl ms-auto"
|
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -687,6 +1148,7 @@ function PrescriptionRow({
|
|||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const primaryMain = theme.palette.primary.main;
|
const primaryMain = theme.palette.primary.main;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="flex items-center gap-4 p-3 rounded-2xl transition-colors duration-200"
|
className="flex items-center gap-4 p-3 rounded-2xl transition-colors duration-200"
|
||||||
@@ -695,10 +1157,7 @@ function PrescriptionRow({
|
|||||||
border: `1px solid ${alpha(primaryMain, 0.08)}`,
|
border: `1px solid ${alpha(primaryMain, 0.08)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i
|
<i className={`${icon} text-2xl shrink-0`} style={{ color: primaryMain }} />
|
||||||
className={`${icon} text-2xl shrink-0`}
|
|
||||||
style={{ color: primaryMain }}
|
|
||||||
/>
|
|
||||||
<Box className="flex-1 min-w-0">
|
<Box className="flex-1 min-w-0">
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
Reference in New Issue
Block a user