This commit is contained in:
2026-03-29 13:40:23 +03:30
parent cef1b5335a
commit 24cb87d94e
29 changed files with 1071 additions and 4887 deletions
+2
View File
@@ -26,3 +26,5 @@ AI_SERVICE_API_KEY=
SENSOR_HUB_SERVICE_BASE_URL=https://sensor-hub.example.com SENSOR_HUB_SERVICE_BASE_URL=https://sensor-hub.example.com
SENSOR_HUB_SERVICE_API_KEY= SENSOR_HUB_SERVICE_API_KEY=
CROP_ZONE_CHUNK_AREA_SQM=10000
-1836
View File
File diff suppressed because it is too large Load Diff
-152
View File
@@ -1,152 +0,0 @@
# مشخصات Response های API بخش Crop Zoning
این سند فقط **فرمت و ساختار response**هایی را که فرانت‌اند از API انتظار دارد شرح می‌دهد.
---
## ۱. API بهینه‌سازی زون‌بندی (Optimize Zoning)
وقتی کاربر منطقه را روی نقشه انتخاب می‌کند و دکمه «بهینه‌سازی مجدد» را می‌زند، فرانت‌اند یک **GeoJSON Polygon** (مختصات منطقه) به API می‌فرستد و انتظار دارد سرور یک **GeoJSON FeatureCollection** برگرداند که هر feature آن یک زون با geometry (چندضلعی) و properties (داده‌های پیشنهاد محصول) دارد.
### Request (خلاصه)
- **ورودی:** یک GeoJSON به صورت `Feature` با `geometry.type: "Polygon"` (مختصات به صورت `[lng, lat]`).
### Response مورد انتظار
یک **GeoJSON FeatureCollection** با این ساختار:
```json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[lng, lat], [lng, lat], ...]]
},
"properties": {
"zoneId": "string",
"crop": "wheat" | "canola" | "saffron",
"matchPercent": number,
"waterNeed": "string",
"estimatedProfit": "string",
"reason": "string",
"criteria": [
{ "name": "string", "value": number },
...
]
}
}
]
}
```
### توضیح فیلدهای `properties` هر زون
| فیلد | نوع | توضیح |
|------|-----|--------|
| `zoneId` | `string` | شناسه یکتا برای زون (مثلاً `"zone-0"`, `"zone-1"`) |
| `crop` | `"wheat" \| "canola" \| "saffron"` | نوع محصول پیشنهادی برای این زون |
| `matchPercent` | `number` | درصد تطابق (۰–۱۰۰) برای پیشنهاد محصول |
| `waterNeed` | `string` | نیاز آبی (مثلاً `"۴۵۰۰-۵۵۰۰ m³/ha"`) |
| `estimatedProfit` | `string` | سود تخمینی (مثلاً `"۱۵-۲۵ میلیون/هکتار"`) |
| `reason` | `string` | توضیح کوتاه دلیل پیشنهاد این محصول |
| `criteria` | `Array<{ name: string, value: number }>` | معیارهای امتیازدهی برای نمودار راداری؛ `value` بین ۰ تا ۱۰۰ (مثلاً دما، بارش، خاک، آب) |
### نمونه response (یک feature)
```json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.38, 35.68],
[51.381, 35.68],
[51.381, 35.681],
[51.38, 35.681],
[51.38, 35.68]
]
]
},
"properties": {
"zoneId": "zone-0",
"crop": "wheat",
"matchPercent": 78,
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار",
"reason": "دمای مناسب، خاک حاصلخیز، دسترسی به آب کافی",
"criteria": [
{ "name": "دما", "value": 85 },
{ "name": "بارش", "value": 72 },
{ "name": "خاک", "value": 80 },
{ "name": "آب", "value": 65 }
]
}
}
]
}
```
---
## ۲. API منطقه اولیه (اختیاری)
اگر بخواهید منطقه اولیه نقشه از سرور بیاید (به‌جای ماک ثابت)، response باید یک **GeoJSON Feature** با Polygon باشد:
```json
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.38, 35.68],
[51.40, 35.68],
[51.40, 35.70],
[51.38, 35.70],
[51.38, 35.68]
]
]
}
}
```
- مختصات به صورت `[longitude, latitude]` (lng, lat).
- آرایه اول `coordinates` حلقه بیرونی چندضلعی است؛ نقطه اول و آخر باید یکسان باشند.
---
## ۳. خلاصه نوع‌های TypeScript (برای تطبیق با بک‌اند)
```ts
type CropType = 'wheat' | 'canola' | 'saffron'
interface ZoneFeatureProperties {
zoneId: string
crop: CropType
matchPercent: number
waterNeed: string
estimatedProfit: string
reason: string
criteria: { name: string; value: number }[]
}
// Response بهینه‌سازی = GeoJSON FeatureCollection
// با Feature<Polygon, ZoneFeatureProperties>
```
---
## ۴. نکات
- **Layer فعلی:** فرانت‌اند لایه‌های مختلف (`crops`, `waterNeed`, `soilQuality`, `cultivationRisk`) دارد؛ در صورت نیاز می‌توان برای هر لایه response جدا یا فیلدهای اضافه در `properties` تعریف کرد.
- **دکمه «تغییر محصول»:** در پنل جزئیات زون، کاربر می‌تواند محصول را بین `wheat`, `canola`, `saffron` عوض کند؛ در صورت نیاز می‌توان API جدا برای ذخیره این تغییر تعریف کرد.
- **بخش آب و هوا:** داده‌های آب و هوا از سرویس جدا (`farmDashboardService.getAllCards()``farmWeatherCard`) گرفته می‌شوند و در این سند پوشش داده نشده‌اند.
-544
View File
@@ -1,544 +0,0 @@
# مستندات APIهای زون‌بندی کشت (Crop Zoning)
این سند تمام APIهای مورد نیاز برای صفحه **Crop Zoning** را شرح می‌دهد: ورودی‌ها، خروجی‌ها، محصولات، رنگ‌ها، مساحت کلی و دیتای هر بخش زمین به صورت جداگانه.
**مسیر صفحه:** `(dashboard)/(private)/crop-zoning`
**کامپوننت اصلی:** `CropZoningWrapper`
---
## نمای کلی و جریان درخواست‌ها
```
۱. GET area → منطقهٔ ثابت (کاربر امکان رسم ندارد)
۲. GET products → لیست محصولات و رنگ‌ها
۳. POST zones/initial → ارسال محدودهٔ مربع‌ها → دیتای محصولات پیشنهادی (نقشه + tooltip)
۴. POST zones/water-need → ارسال محدودهٔ مربع‌ها → نیاز آبی هر منطقه
۵. POST zones/soil-quality → ارسال محدودهٔ مربع‌ها → کیفیت خاک هر منطقه
۶. POST zones/cultivation-risk → ارسال محدودهٔ مربع‌ها → ریسک کشت هر منطقه
۷. GET zone/:zoneId → کلیک روی مربع → دیتای تکمیلی (پنل جزئیات: reason, criteria, ...)
```
| ردیف | API | هدف |
|------|-----|------|
| ۱ | **منطقهٔ اولیه** | دریافت منطقهٔ زمین به صورت GeoJSON؛ کاربر نمی‌تواند چیزی رسم کند |
| ۲ | **لیست محصولات و رنگ‌ها** | دریافت محصولات قابل کشت به همراه رنگ نمایش و لیبل فارسی |
| ۳ | **دیتای اولیه زون‌ها (محصولات)** | ارسال محدودهٔ مربع‌ها، دریافت محصول پیشنهادی برای نقشه و tooltip |
| ۴ | **نیاز آبی** | ارسال محدودهٔ مربع‌ها، دریافت نیاز آبی هر منطقه برای لایهٔ نیاز آبی |
| ۵ | **کیفیت خاک** | ارسال محدودهٔ مربع‌ها، دریافت کیفیت خاک هر منطقه برای لایهٔ کیفیت خاک |
| ۶ | **ریسک کشت** | ارسال محدودهٔ مربع‌ها، دریافت ریسک کشت هر منطقه برای لایهٔ ریسک کشت |
| ۷ | **دیتای تکمیلی زون** | با کلیک روی هر مربع، دریافت دیتای جزئیات (دلیل، معیارها، نمودار) |
---
## ۰. API منطقهٔ اولیه (Area)
منطقهٔ ثابت زمین که از بک‌اند دریافت می‌شود. کاربر امکان رسم یا ویرایش منطقه را ندارد.
### ۰.۱ مشخصات
- **متد:** `GET`
- **آدرس پیشنهادی:** `GET /api/crop-zoning/area/`
- **هدف:** دریافت polygon منطقهٔ زمین برای نمایش روی نقشه.
### ۰.۲ ورودی (Request)
بدون پارامتر.
### ۰.۳ خروجی (Response Body)
```json
{
"status": "success",
"data": {
"area": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.38, 35.68],
[51.40, 35.68],
[51.40, 35.70],
[51.38, 35.70],
[51.38, 35.68]
]
]
}
}
}
}
```
- **مختصات:** `[longitude, latitude]` طبق استاندارد GeoJSON
---
## ۱. API لیست محصولات و رنگ‌ها
برای نمایش راهنمای رنگ‌ها (Legend) و dropdown انتخاب محصول در پنل جزئیات هر زون.
### ۱.۱ مشخصات
- **متد:** `GET`
- **آدرس پیشنهادی:** `GET /api/crop-zoning/products/` یا `GET /api/crops/`
- **هدف:** دریافت لیست محصولات قابل کشت با رنگ و لیبل نمایشی.
### ۱.۲ ورودی (Request)
بدون پارامتر یا با پارامترهای اختیاری:
| پارامتر | نوع | اجباری | توضیح |
|---------|-----|--------|--------|
| `locale` | string | خیر | کد زبان برای لیبل‌ها (مثلاً `fa`, `en`) |
### ۱.۳ خروجی (Response)
```json
{
"status": "success",
"data": {
"products": [
{
"id": "wheat",
"label": "گندم",
"color": "#6bcb77"
},
{
"id": "canola",
"label": "کلزا",
"color": "#ffd93d"
},
{
"id": "saffron",
"label": "زعفران",
"color": "#9b59b6"
}
]
}
}
```
**ساختار هر محصول:**
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `id` | string | بله | شناسهٔ یکتا (مثلاً `wheat`, `canola`, `saffron`) |
| `label` | string | بله | نام نمایشی به زبان کاربر |
| `color` | string | بله | رنگ hex برای نمایش روی نقشه و Legend |
---
## ۲. API دیتای اولیه زون‌ها
با رسم منطقهٔ زمین، فرانت **محدودهٔ همهٔ مربع‌ها** (گرید داخل polygon) را ارسال می‌کند و **دیتای اولیه** هر مربع را دریافت می‌کند — برای نمایش نقشه، رنگ‌بندی، و **هاور/tooltip**. این دیتا شامل `reason` و `criteria` **نیست**.
### ۲.۱ مشخصات
- **متد:** `POST`
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/initial/`
- **هدف:** ارسال محدودهٔ مربع‌ها، دریافت دیتای اولیه برای نقشه، هاور و tooltip.
### ۲.۲ ورودی (Request Body)
فرانت ابتدا با Turf.js از روی polygon منطقه گرید می‌سازد، سپس `FeatureCollection` همهٔ polygonهای مربع‌ها را ارسال می‌کند.
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `zones` | GeoJSON FeatureCollection | بله | محدودهٔ هر مربع به صورت Polygon؛ ترتیب index با پاسخ یکسان است |
| `products` | string[] | خیر | لیست محصولات مدنظر؛ در صورت عدم ارسال از همهٔ محصولات استفاده شود |
**ساختار `zones` (محدودهٔ همهٔ مربع‌ها):**
```json
{
"zones": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.38, 35.68],
[51.3815, 35.68],
[51.3815, 35.6815],
[51.38, 35.6815],
[51.38, 35.68]
]
]
},
"properties": { "index": 0 }
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.3815, 35.68],
[51.383, 35.68],
[51.383, 35.6815],
[51.3815, 35.6815],
[51.3815, 35.68]
]
]
},
"properties": { "index": 1 }
}
]
},
"products": ["wheat", "canola", "saffron"]
}
```
- **مختصات:** `[longitude, latitude]` طبق استاندارد GeoJSON
- **index:** در `properties` هر feature برای تطابق با پاسخ (اختیاری؛ در صورت نبودن از ترتیب آرایه استفاده شود)
### ۲.۳ خروجی (Response Body) — دیتای اولیه
فقط فیلدهای لازم برای **نقشه**، **هاور** و **tooltip** (نمایش هنگام عبور ماوس روی هر مربع)؛ بدون `reason` و `criteria`.
```json
{
"status": "success",
"data": {
"total_area_hectares": 23.45,
"total_area_sqm": 234500,
"zone_count": 3,
"zones": [
{
"zoneId": "zone-0",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[51.38, 35.68],
[51.3815, 35.68],
[51.3815, 35.6815],
[51.38, 35.6815],
[51.38, 35.68]
]
]
},
"crop": "wheat",
"matchPercent": 85,
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار"
},
{
"zoneId": "zone-1",
"geometry": { "type": "Polygon", "coordinates": [...] },
"crop": "canola",
"matchPercent": 78,
"waterNeed": "۵۰۰۰-۶۰۰۰ m³/ha",
"estimatedProfit": "۲۰-۳۵ میلیون/هکتار"
}
]
}
}
```
**ساختار دیتای اولیه هر زون (هم برای نقشه هم برای هاور/tooltip):**
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `zoneId` | string | بله | شناسهٔ یکتا برای درخواست دیتای تکمیلی |
| `geometry` | Polygon | بله | هندسهٔ همان مربع ارسالی |
| `crop` | string \| null | خیر | محصول پیشنهادی؛ اگر `null`/خالی/`uncultivable` باشد → زون **غیرقابل کشت** و رنگ خاکستری |
| `matchPercent` | number | خیر | درصد تطابق (هاور/tooltip) |
| `waterNeed` | string | خیر | نیاز آبی (هاور/tooltip) |
| `estimatedProfit` | string | خیر | سود تخمینی (هاور/tooltip) |
**زون غیرقابل کشت:** اگر برای مربعی اطلاعاتی نیاید یا `crop` خالی/`null`/`uncultivable` باشد، آن مربع خاکستری نمایش داده شده و در tooltip «غیر قابل کشت» نشان داده می‌شود. کلیک روی آن پنل جزئیات باز نمی‌شود.
**نکته:** این فیلدها هنگام **هاور** روی مربع در tooltip نمایش داده می‌شوند؛ نیازی به درخواست جداگانه برای tooltip نیست.
---
## ۲.۱ API نیاز آبی (Water Need)
نیاز آبی هر منطقه را بر اساس محدودهٔ مربع‌ها برمی‌گرداند. با تغییر لایه به «نیاز آبی» در LayerControl، فرانت این API را صدا می‌زند و نقشه و Legend را به‌روزرسانی می‌کند.
### مشخصات
- **متد:** `POST`
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/water-need/`
- **هدف:** دریافت نیاز آبی هر منطقه برای نمایش روی نقشه در لایهٔ نیاز آبی.
### ورودی (Request Body)
همان ساختار `POST zones/initial/`:
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `zones` | GeoJSON FeatureCollection | بله | محدودهٔ هر مربع به صورت Polygon |
### خروجی (Response Body)
```json
{
"status": "success",
"data": {
"zones": [
{
"zoneId": "zone-0",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "low",
"value": "۳۰۰۰-۴۰۰۰ m³/ha",
"color": "#7dd3fc"
},
{
"zoneId": "zone-1",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "medium",
"value": "۵۰۰۰-۶۰۰۰ m³/ha",
"color": "#0ea5e9"
}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `zoneId` | string | شناسهٔ زون |
| `geometry` | Polygon | هندسهٔ مربع |
| `level` | string | سطح: `low`, `medium`, `high` |
| `value` | string | مقدار نیاز آبی (مثلاً m³/ha) |
| `color` | string | رنگ hex برای نمایش |
---
## ۲.۲ API کیفیت خاک (Soil Quality)
کیفیت خاک هر منطقه را برمی‌گرداند. با تغییر لایه به «کیفیت خاک»، فرانت این API را صدا می‌زند.
### مشخصات
- **متد:** `POST`
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/soil-quality/`
### ورودی (Request Body)
همان `zones` (FeatureCollection).
### خروجی (Response Body)
```json
{
"status": "success",
"data": {
"zones": [
{
"zoneId": "zone-0",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "low",
"score": 35,
"color": "#f87171"
},
{
"zoneId": "zone-1",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "high",
"score": 85,
"color": "#22c55e"
}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `level` | string | سطح: `low`, `medium`, `high` |
| `score` | number | امتیاز ۰–۱۰۰ |
| `color` | string | رنگ hex |
---
## ۲.۳ API ریسک کشت (Cultivation Risk)
ریسک کشت هر منطقه را برمی‌گرداند. با تغییر لایه به «ریسک کشت»، فرانت این API را صدا می‌زند.
### مشخصات
- **متد:** `POST`
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/cultivation-risk/`
### ورودی و خروجی
ورودی: همان `zones` (FeatureCollection).
خروجی نمونه:
```json
{
"status": "success",
"data": {
"zones": [
{
"zoneId": "zone-0",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "low",
"color": "#22c55e"
},
{
"zoneId": "zone-1",
"geometry": { "type": "Polygon", "coordinates": [...] },
"level": "high",
"color": "#ef4444"
}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `level` | string | سطح: `low`, `medium`, `high` |
| `color` | string | رنگ hex |
**نکته:** برای هر لایه (نیاز آبی، کیفیت خاک، ریسک کشت) فرانت یک **درخواست جداگانه** ارسال می‌کند و نقشه و Legend متناسب با همان لایه به‌روزرسانی می‌شوند.
---
## ۳. API دیتای تکمیلی زون (با کلیک روی مربع)
وقتی کاربر روی یک مربع کلیک می‌کند، فرانت با `zoneId` دیتای **تکمیلی** را درخواست می‌کند — برای نمایش پنل جزئیات: دلیل پیشنهاد، معیارها، نمودار راداری.
### ۳.۱ مشخصات
- **متد:** `GET`
- **آدرس پیشنهادی:** `GET /api/crop-zoning/zones/:zoneId/details/`
- **هدف:** دریافت دیتای تکمیلی یک زون برای پنل جزئیات.
### ۳.۲ ورودی (Request)
| پارامتر | محل | نوع | اجباری | توضیح |
|---------|------|-----|--------|--------|
| `zoneId` | path | string | بله | شناسهٔ زون (مثلاً `zone-0`) |
**مثال:** `GET /api/crop-zoning/zones/zone-0/details/`
### ۳.۳ خروجی (Response Body)
```json
{
"status": "success",
"data": {
"zoneId": "zone-0",
"crop": "wheat",
"matchPercent": 85,
"waterNeed": "۴۵۰۰-۵۵۰۰ m³/ha",
"estimatedProfit": "۱۵-۲۵ میلیون/هکتار",
"reason": "دمای مناسب، خاک حاصلخیز، دسترسی به آب کافی",
"criteria": [
{ "name": "دما", "value": 82 },
{ "name": "بارش", "value": 75 },
{ "name": "خاک", "value": 88 },
{ "name": "آب", "value": 70 }
],
"area_hectares": 2.25
}
}
```
**فیلدهای دیتای تکمیلی:**
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `zoneId` | string | بله | همان zoneId درخواست |
| `crop` | string | بله | محصول پیشنهادی |
| `matchPercent` | number | بله | درصد تطابق |
| `waterNeed` | string | بله | نیاز آبی |
| `estimatedProfit` | string | بله | سود تخمینی |
| `reason` | string | بله | **فقط در دیتای تکمیلی** — دلیل پیشنهاد محصول |
| `criteria` | object[] | بله | **فقط در دیتای تکمیلی** — معیارها برای نمودار راداری |
| `area_hectares` | number | خیر | مساحت این زون بر حسب هکتار |
### ۳.۴ ساختار `criteria`
| فیلد | نوع | توضیح |
|------|-----|--------|
| `name` | string | نام معیار (دما، بارش، خاک، آب) |
| `value` | number | امتیاز ۰–۱۰۰ |
---
## ۴. مساحت کلی (Total Area)
در پاسخ **API دیتای اولیه زون‌ها** برمی‌گردد:
| فیلد | نوع | توضیح |
|------|-----|--------|
| `total_area_hectares` | number | مساحت کل منطقه بر حسب هکتار |
| `total_area_sqm` | number | مساحت کل بر حسب متر مربع |
---
## ۵. خلاصهٔ ساختار‌های مورد نیاز فرانت
### دیتای اولیه زون (برای نقشه و هاور/tooltip)
```ts
interface ZoneInitialData {
zoneId: string
geometry: Polygon
crop: string
matchPercent: number
waterNeed: string
estimatedProfit: string
}
```
### دیتای تکمیلی زون (برای پنل جزئیات — پس از کلیک)
```ts
interface ZoneDetailData {
zoneId: string
crop: string
matchPercent: number
waterNeed: string
estimatedProfit: string
reason: string
criteria: { name: string; value: number }[]
area_hectares?: number
}
```
### محصولات و رنگ‌ها (پیش‌فرض فرانت)
```ts
const CROP_COLORS: Record<CropType, string> = {
wheat: '#6bcb77',
canola: '#ffd93d',
saffron: '#9b59b6'
}
```
---
## ۶. جریان فرانت با APIها
1. **لود صفحه:** `GET /api/crop-zoning/products/` → لیست محصولات و رنگ‌ها.
2. **رسم منطقه / بهینه‌سازی:** فرانت با Turf از polygon منطقه گرید می‌سازد → `POST /api/crop-zoning/zones/initial/` با `zones` (FeatureCollection) → نقشه و tooltip با دیتای محصولات رسم می‌شود.
3. **تغییر لایه در LayerControl:** برای هر لایه یک درخواست جداگانه ارسال می‌شود:
- محصولات پیشنهادی: `POST zones/initial/` (در مرحلهٔ ۲)
- نیاز آبی: `POST zones/water-need/` → نقشه و Legend به‌روزرسانی می‌شوند
- کیفیت خاک: `POST zones/soil-quality/` → نقشه و Legend به‌روزرسانی می‌شوند
- ریسک کشت: `POST zones/cultivation-risk/` → نقشه و Legend به‌روزرسانی می‌شوند
4. **کلیک روی مربع:** `GET /api/crop-zoning/zones/{zoneId}/details/` → دیتای تکمیلی → پنل جزئیات باز می‌شود (reason, criteria, نمودار راداری).
---
## ۷. وضعیت فعلی و نیازمندی‌ها
- در حال حاضر زون‌بندی با **دیتای ماک** و الگوریتم محلی (`createZonedGrid` در `cropZoningUtils.ts`) کار می‌کند.
- برای اتصال به بک‌اند، لازم است:
1. سرویس `cropZoningService` با سه endpoint: `getProducts()`, `getZonesInitial(zones)`, `getZoneDetails(zoneId)` ایجاد شود.
2. در `CropZoningMap` به جای `createZonedGrid` ابتدا گرید با Turf ساخته شود، سپس `zones` به API ارسال و پاسخ برای رسم استفاده شود.
3. در `onZoneClick` قبل از باز کردن پنل، `getZoneDetails(zoneId)` صدا زده شود و دیتای تکمیلی به `ZoneDetailPanel` پاس داده شود.
4. مساحت کلی (`total_area_hectares`) در پاسخ initial در UI نمایش داده شود.
-653
View File
@@ -1,653 +0,0 @@
# مستندات API داشبورد Farm (Farm Dashboard)
این سند شامل توضیحات کل داشبورد، APIهای تنظیمات (disable/enable/move کارت‌ها) و ساختار پیشنهادی ریسپانس برای محتوای کارت‌ها است.
---
## ۱. نمای کلی داشبورد
داشبورد Farm از کامپوننت `FarmDashboardWrapper` استفاده می‌کند و شامل ردیف‌ها (rows) و کارت‌های (cards) زیر است:
| Row ID | Row Label | کارت‌ها |
|--------|-----------|---------|
| `overviewKpis` | Overview KPIs | `farmOverviewKpis` |
| `weatherAlerts` | Weather & Alerts | `farmWeatherCard`, `farmAlertsTracker` |
| `sensorMonitoring` | Sensor Monitoring | `sensorValuesList`, `sensorRadarChart` |
| `sensorCharts` | Sensor Charts | `sensorComparisonChart`, `anomalyDetectionCard` |
| `alertsWater` | Alerts & Water Prediction | `farmAlertsTimeline`, `waterNeedPrediction` |
| `predictions` | Predictions | `harvestPredictionCard`, `yieldPredictionChart` |
| `soilHeatmap` | Soil Moisture Heatmap | `soilMoistureHeatmap` |
| `ndviRecommendations` | NDVI & Recommendations | `ndviHealthCard`, `recommendationsList` |
| `economic` | Economic Overview | `economicOverview` |
---
## ۲. APIهای تنظیمات داشبورد
### ۲.۱ دریافت تنظیمات داشبورد (Get Config)
```
GET /api/farm-dashboard-config
```
**توضیح:** تنظیمات شخصی‌سازی داشبورد کاربر لاگین‌شده را برمی‌گرداند.
**Response:**
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
}
```
**فیلدها:**
| فیلد | نوع | توضیح |
|------|-----|-------|
| `disabled_card_ids` | `string[]` | لیست شناسه کارت‌های غیرفعال (hidden) |
| `row_order` | `string[]` | ترتیب نمایش ردیف‌ها |
| `enable_drag_reorder` | `boolean` | امکان جابجایی ردیف‌ها با drag |
---
### ۲.۲ غیرفعال کردن کارت (Disable Card)
```
PATCH /api/farm-dashboard-config
```
**Request Body:**
```json
{
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"]
}
```
کارت با شناسه `cardId` به لیست `disabled_card_ids` اضافه می‌شود و در داشبورد نمایش داده نمی‌شود.
---
### ۲.۳ فعال کردن کارت (Enable Card)
```
PATCH /api/farm-dashboard-config
```
**Request Body:**
```json
{
"disabled_card_ids": ["farmWeatherCard"]
}
```
شناسه کارت از لیست `disabled_card_ids` حذف می‌شود و کارت دوباره نمایش داده می‌شود.
**نکته:** کل لیست `disabled_card_ids` جدید ارسال می‌شود؛ برای enable باید آرایه بدون آن کارت فرستاده شود.
---
### ۲.۴ جابجایی ردیف‌ها (Move Rows)
```
PATCH /api/farm-dashboard-config
```
**Request Body:**
```json
{
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"predictions",
"sensorCharts",
"alertsWater",
"soilHeatmap",
"ndviRecommendations",
"economic"
]
}
```
ترتیب ردیف‌ها طبق آرایه `row_order` ذخیره می‌شود.
---
### ۲.۵ تغییر وضعیت Drag Reorder
```
PATCH /api/farm-dashboard-config
```
**Request Body:**
```json
{
"enable_drag_reorder": false
}
```
---
## ۳. API دریافت همه دیتای کارت‌ها
### Endpoint پیشنهادی
```
GET /api/farm-dashboard
```
یا به تفکیک کارت:
```
GET /api/farm-dashboard/cards
```
---
## ۴. لیست کامل ریسپانس هر کارت
ساختار پیشنهادی response برای محتوای هر کارت (بر اساس داده‌های mock فعلی در فرانت):
### ۴.۱ farmOverviewKpis
```json
{
"kpis": [
{
"id": "farm_health_score",
"title": "Farm Health Score",
"subtitle": "AI Analysis",
"stats": "87%",
"avatarColor": "success",
"avatarIcon": "tabler-heartbeat",
"chipText": "Good",
"chipColor": "success"
},
{
"id": "water_stress_index",
"title": "Water Stress Index",
"subtitle": "Current",
"stats": "12%",
"avatarColor": "info",
"avatarIcon": "tabler-droplet",
"chipText": "Low",
"chipColor": "success"
},
{
"id": "disease_risk",
"title": "Disease Risk",
"subtitle": "Last 7 Days",
"stats": "Low",
"avatarColor": "success",
"avatarIcon": "tabler-bug",
"chipText": "5%",
"chipColor": "success"
},
{
"id": "avg_soil_moisture",
"title": "Avg Soil Moisture",
"subtitle": "Field-wide",
"stats": "65%",
"avatarColor": "primary",
"avatarIcon": "tabler-plant-2",
"chipText": "Optimal",
"chipColor": "success"
},
{
"id": "yield_prediction",
"title": "Yield Prediction",
"subtitle": "This Season",
"stats": "42 ton",
"avatarColor": "secondary",
"avatarIcon": "tabler-chart-bar",
"chipText": "+8%",
"chipColor": "success"
},
{
"id": "pest_risk",
"title": "Pest Risk",
"subtitle": "AI Forecast",
"stats": "15%",
"avatarColor": "warning",
"avatarIcon": "tabler-bug-off",
"chipText": "Monitor",
"chipColor": "warning"
}
]
}
```
---
### ۴.۲ farmWeatherCard
```json
{
"condition": "Clear",
"temperature": 24,
"unit": "°C",
"humidity": 45,
"windSpeed": 12,
"windUnit": "km/h",
"chartData": {
"labels": ["6am", "9am", "12pm", "3pm", "6pm", "9pm", "12am"],
"series": [[18, 22, 26, 28, 25, 20, 18]]
}
}
```
---
### ۴.۳ farmAlertsTracker
```json
{
"totalAlerts": 3,
"radialBarValue": 30,
"alertStats": [
{
"title": "Water Shortage",
"count": "2",
"avatarColor": "error",
"avatarIcon": "tabler-droplet-half-2"
},
{
"title": "Fungal Risk",
"count": "1",
"avatarColor": "warning",
"avatarIcon": "tabler-mushroom"
},
{
"title": "Frost Alert",
"count": "0",
"avatarColor": "info",
"avatarIcon": "tabler-snowflake"
}
]
}
```
---
### ۴.۴ sensorValuesList
```json
{
"sensors": [
{
"title": "28°C",
"subtitle": "Air Temperature",
"trendNumber": 2.1,
"trend": "positive",
"unit": "°C"
},
{
"title": "24°C",
"subtitle": "Soil Temperature",
"trendNumber": -0.5,
"trend": "negative",
"unit": "°C"
},
{
"title": "65%",
"subtitle": "Air Humidity",
"trendNumber": 3.2,
"trend": "positive",
"unit": "%"
},
{
"title": "42%",
"subtitle": "Soil Moisture (10cm)",
"trendNumber": -1.8,
"trend": "negative",
"unit": "%"
},
{
"title": "6.8",
"subtitle": "Soil pH",
"trendNumber": 0.2,
"trend": "positive",
"unit": "pH"
},
{
"title": "1.2",
"subtitle": "EC (dS/m)",
"trendNumber": 0.1,
"trend": "positive",
"unit": "dS/m"
},
{
"title": "850",
"subtitle": "Light Intensity (lux)",
"trendNumber": 15.3,
"trend": "positive",
"unit": "lux"
},
{
"title": "12",
"subtitle": "Wind Speed (km/h)",
"trendNumber": -2.4,
"trend": "negative",
"unit": "km/h"
}
]
}
```
---
### ۴.۵ sensorRadarChart
```json
{
"labels": ["Temp", "Humidity", "pH", "EC", "Light", "Wind"],
"series": [
{ "name": "Today", "data": [75, 65, 80, 70, 85, 60] },
{ "name": "Ideal", "data": [80, 70, 75, 75, 90, 50] }
]
}
```
---
### ۴.۶ sensorComparisonChart
```json
{
"currentValue": 48,
"vsLastWeek": "+5%",
"vsLastWeekValue": 5,
"categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"series": [
{ "name": "Today", "data": [42, 45, 48, 52, 50, 48, 46] },
{ "name": "Last Week", "data": [38, 40, 42, 45, 43, 40, 38] }
]
}
```
---
### ۴.۷ anomalyDetectionCard
```json
{
"anomalies": [
{
"sensor": "Soil Moisture Z3",
"value": "38%",
"expected": "45-65%",
"deviation": "-12%",
"severity": "warning"
},
{
"sensor": "pH Sector 2",
"value": "5.2",
"expected": "6.0-7.0",
"deviation": "-0.8",
"severity": "error"
}
]
}
```
---
### ۴.۸ farmAlertsTimeline
```json
{
"alerts": [
{
"title": "Water Shortage Risk",
"description": "Soil moisture at 10cm depth (42%) is below optimal. AI predicts stress in 2-3 days if no irrigation. Recommended: irrigate within 24h.",
"time": "15 min ago",
"color": "warning"
},
{
"title": "Fungal Disease Risk",
"description": "High humidity (65%) + temp 24°C creates favorable conditions for fungal growth. Consider preventive fungicide or reduce irrigation.",
"time": "1 hour ago",
"color": "error"
},
{
"title": "Irrigation Suggestion",
"description": "Optimal watering window: 6:00-8:00 AM. Suggested amount: 450 m³ for Zone A. Expected efficiency gain: 12%.",
"time": "2 hours ago",
"color": "info"
},
{
"title": "Soil Salinity Check",
"description": "EC reading 1.2 dS/m is within range. No action needed. Next check recommended in 5 days.",
"time": "4 hours ago",
"color": "success"
}
]
}
```
---
### ۴.۹ waterNeedPrediction
```json
{
"totalNext7Days": 3290,
"unit": "m³",
"categories": ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6", "Day 7"],
"series": [{ "name": "Water Need", "data": [420, 450, 480, 460, 490, 510, 480] }]
}
```
---
### ۴.۱۰ harvestPredictionCard
```json
{
"date": "2025-10-15",
"dateFormatted": "Oct 15, 2025",
"daysUntil": 58,
"description": "Based on current GDD accumulation and weather forecast. Optimal harvest window: Oct 12-18.",
"optimalWindowStart": "2025-10-12",
"optimalWindowEnd": "2025-10-18"
}
```
---
### ۴.۱۱ yieldPredictionChart
```json
{
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"series": [
{ "name": "This Year", "data": [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42] },
{ "name": "Last Year", "data": [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38] }
],
"summary": [
{ "title": "Predicted Yield", "subtitle": "This Season", "amount": "42 ton", "avatarColor": "primary", "avatarIcon": "tabler-chart-bar" },
{ "title": "Harvest Date", "subtitle": "Est. Oct 15", "amount": "+8%", "avatarColor": "success", "avatarIcon": "tabler-calendar" }
]
}
```
---
### ۴.۱۲ soilMoistureHeatmap
```json
{
"zones": ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7"],
"hours": ["6h", "8h", "10h", "12h", "14h", "16h", "18h"],
"series": [
{ "name": "Z1", "data": [{"x": "6h", "y": 52}, {"x": "8h", "y": 48}, {"x": "10h", "y": 55}, {"x": "12h", "y": 60}, {"x": "14h", "y": 58}, {"x": "16h", "y": 54}, {"x": "18h", "y": 50}] },
{ "name": "Z2", "data": [{"x": "6h", "y": 45}, {"x": "8h", "y": 42}, {"x": "10h", "y": 48}, {"x": "12h", "y": 52}, {"x": "14h", "y": 50}, {"x": "16h", "y": 47}, {"x": "18h", "y": 44}] }
]
}
```
---
### ۴.۱۳ ndviHealthCard
```json
{
"ndviIndex": 0.78,
"healthData": [
{ "title": "Nitrogen Stress", "value": "Low", "color": "success", "icon": "tabler-leaf" },
{ "title": "Crop Health", "value": "Good", "color": "success", "icon": "tabler-plant" }
]
}
```
---
### ۴.۱۴ recommendationsList
```json
{
"recommendations": [
{
"title": "Irrigation: 6:00-8:00 AM",
"subtitle": "450 m³ for Zone A. Without irrigation, yield may drop ~8%.",
"avatarIcon": "tabler-droplet",
"avatarColor": "primary"
},
{
"title": "Fertilizer: NPK 20-20-20",
"subtitle": "Apply 25 kg/ha in 7 days. Current N deficiency in sector 2.",
"avatarIcon": "tabler-leaf",
"avatarColor": "success"
},
{
"title": "Fungicide: Preventive",
"subtitle": "Humidity + temp favor fungi. Consider copper-based spray.",
"avatarIcon": "tabler-mushroom",
"avatarColor": "warning"
},
{
"title": "Harvest Window: Oct 12-18",
"subtitle": "Peak ripeness expected Oct 15. Plan labor accordingly.",
"avatarIcon": "tabler-calendar-event",
"avatarColor": "info"
}
]
}
```
---
### ۴.۱۵ economicOverview
```json
{
"economicData": [
{ "title": "Water Cost", "value": "€720", "subtitle": "This month", "avatarIcon": "tabler-droplet", "avatarColor": "primary" },
{ "title": "AI Water Savings", "value": "€156", "subtitle": "18% saved", "avatarIcon": "tabler-bulb", "avatarColor": "success" },
{ "title": "Platform ROI", "value": "127%", "subtitle": "vs last year", "avatarIcon": "tabler-chart-line", "avatarColor": "info" },
{ "title": "Income Forecast", "value": "€42k", "subtitle": "This season", "avatarIcon": "tabler-currency-euro", "avatarColor": "success" }
],
"chartSeries": [
{ "name": "Water Cost", "data": [120, 115, 110, 125, 118, 122] },
{ "name": "Fertilizer", "data": [80, 85, 90, 75, 82, 78] }
],
"chartCategories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
}
```
---
## ۵. Response یکپارچه همه کارت‌ها
اگر یک endpoint برای کل دیتای داشبورد داشته باشید:
```
GET /api/farm-dashboard
```
**Response پیشنهادی:**
```json
{
"code": 200,
"msg": "OK",
"data": {
"farmOverviewKpis": { ... },
"farmWeatherCard": { ... },
"farmAlertsTracker": { ... },
"sensorValuesList": { ... },
"sensorRadarChart": { ... },
"sensorComparisonChart": { ... },
"anomalyDetectionCard": { ... },
"farmAlertsTimeline": { ... },
"waterNeedPrediction": { ... },
"harvestPredictionCard": { ... },
"yieldPredictionChart": { ... },
"soilMoistureHeatmap": { ... },
"ndviHealthCard": { ... },
"recommendationsList": { ... },
"economicOverview": { ... }
}
}
```
---
## ۶. خلاصه Endpoints
| عملیات | Method | Endpoint | Body |
|--------|--------|----------|------|
| دریافت تنظیمات | GET | `/api/farm-dashboard-config` | - |
| غیرفعال کردن کارت | PATCH | `/api/farm-dashboard-config` | `{ "disabled_card_ids": [...] }` |
| فعال کردن کارت | PATCH | `/api/farm-dashboard-config` | `{ "disabled_card_ids": [...] }` |
| جابجایی ردیف‌ها | PATCH | `/api/farm-dashboard-config` | `{ "row_order": [...] }` |
| Enable/Disable Drag | PATCH | `/api/farm-dashboard-config` | `{ "enable_drag_reorder": boolean }` |
| دیتای همه کارت‌ها | GET | `/api/farm-dashboard` یا `/api/farm-dashboard/cards` | - |
---
## ۷. Card IDs معتبر
```
farmOverviewKpis
farmWeatherCard
farmAlertsTracker
sensorValuesList
sensorRadarChart
sensorComparisonChart
anomalyDetectionCard
farmAlertsTimeline
waterNeedPrediction
harvestPredictionCard
yieldPredictionChart
soilMoistureHeatmap
ndviHealthCard
recommendationsList
economicOverview
```
## ۸. Row IDs معتبر
```
overviewKpis
weatherAlerts
sensorMonitoring
sensorCharts
alertsWater
predictions
soilHeatmap
ndviRecommendations
economic
```
-192
View File
@@ -1,192 +0,0 @@
# مستندات APIهای دستیار هوشمند مزرعه (Farm AI Assistant)
این سند تمام APIهای مورد نیاز برای صفحه **Farm AI Assistant** را شرح می‌دهد: ورودی‌ها، خروجی‌ها و استفاده در UI.
**مسیر صفحه:** `(dashboard)/(private)/farm-ai-assistant`
**کامپوننت اصلی:** `FarmAiAssistantChat`
---
## نمای کلی
دستیار هوشمند مزرعه برای کار به موارد زیر نیاز دارد:
| ردیف | API | هدف |
|------|-----|------|
| ۱ | **ارسال پیام به دستیار (Chat/Complete)** | دریافت پاسخ ساخت‌یافته (توصیه، لیست، هشدار) بر اساس پیام کاربر و زمینه مزرعه |
| ۲ | **دریافت زمینه مزرعه (Farm Context)** | پر کردن نوار «زمینه مزرعه» (نوع خاک، EC آب، محصول، مرحله رشد، آخرین آبیاری) |
| ۳ | **توصیه آبیاری** | در صورت درخواست کاربر یا تصمیم دستیار برای توصیه آبیاری |
| ۴ | **توصیه کوددهی** | در صورت درخواست کاربر یا توصیه کود |
| ۵ | **تشخیص آفت از تصویر** | وقتی کاربر تصویر گیاه را ارسال می‌کند |
---
## ۱. API ارسال پیام به دستیار (Farm AI Chat)
این API هسته اصلی دستیار است و در حال حاضر در فرانت با پاسخ دمو شبیه‌سازی شده است؛ باید با API واقعی جایگزین شود.
### ۱.۱ مشخصات
- **متد:** `POST`
- **آدرس پیشنهادی:** `POST /api/farm-ai-assistant/chat/` یا `POST /api/farm-ai-assistant/messages/`
- **هدف:** ارسال پیام کاربر (و در صورت وجود تصویر) به همراه زمینه مزرعه و دریافت پاسخ ساخت‌یافته دستیار.
### ۱.۲ ورودی (Request Body)
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `content` | string | بله | متن پیام کاربر |
| `images` | string[] یا base64[] | خیر | آرایه آدرس تصاویر یا داده base64 (در صورت استفاده از آپلود تصویر دوربین در چت) |
| `conversation_id` | string | خیر | شناسه مکالمه برای ادامه گفتگو؛ در اولین پیام ارسال نشود |
| `farm_context` | object | توصیه | زمینه مزرعه برای پاسخ شخصی‌سازی‌شده (در صورت نبودن، بک‌اند می‌تواند از پیش‌فرض استفاده کند) |
ساختار پیشنهادی `farm_context` (هم‌خوان با `FarmContext` در فرانت):
```json
{
"content": "برنامه آبیاری برای گوجه در مرحله گلدهی چطور باشد؟",
"farm_context": {
"soilType": "Loamy",
"waterEC": "1.2 dS/m",
"selectedCrop": "Tomato",
"growthStage": "Flowering",
"lastIrrigationStatus": "2 days ago"
}
}
```
اگر از **تصویر** استفاده شود (دکمه دوربین در input):
```json
{
"content": "این برگ زرد شده، چه مشکلی داره؟",
"images": ["data:image/jpeg;base64,..."],
"farm_context": { ... }
}
```
### ۱.۳ خروجی (Response Body)
پاسخ باید شامل **بخش‌های ساخت‌یافته** (sections) باشد تا در UI به صورت کارت (توصیه، لیست، هشدار) رندر شود.
**قالب پیشنهادی:**
```json
{
"status": "success",
"data": {
"message_id": "a-1739123456789",
"conversation_id": "conv-abc123",
"content": "",
"sections": [
{
"type": "recommendation",
"title": "Irrigation recommendation",
"icon": "droplet",
"frequency": "3 times per week",
"amount": "1520 L per plant",
"timing": "Early morning (05:0007:00)",
"expandableExplanation": "Your loamy soil holds moisture well..."
},
{
"type": "list",
"title": "Key points",
"icon": "leaf",
"items": [
"Avoid midday watering to reduce evaporation",
"Drip irrigation preferred for root zone targeting"
]
},
{
"type": "warning",
"title": "Weather advisory",
"icon": "warning",
"content": "High temps forecasted next week. Consider increasing frequency."
}
]
}
}
```
**ساختار هر بخش (Section) مطابق `AIResponseSection` در فرانت:**
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|--------|
| `type` | string | بله | یکی از: `text` \| `list` \| `recommendation` \| `warning` |
| `title` | string | خیر | عنوان بخش |
| `content` | string | خیر | برای `type: "text"` یا `type: "warning"` |
| `items` | string[] | خیر | برای `type: "list"` |
| `icon` | string | خیر | یکی از: `droplet` \| `leaf` \| `warning` \| `fertilizer` \| `calendar` |
| `frequency` | string | خیر | فقط برای `recommendation`: تعداد دفعات (مثلاً در هفته) |
| `amount` | string | خیر | فقط برای `recommendation`: مقدار (مثلاً لیتر یا کیلوگرم) |
| `timing` | string | خیر | فقط برای `recommendation`: زمان پیشنهادی |
| `expandableExplanation` | string | خیر | فقط برای `recommendation`: توضیح قابل گسترش «چرا این توصیه» |
- اگر `content` خالی باشد و فقط `sections` برگردد، در UI فقط کارت‌ها نمایش داده می‌شوند (مطابق پیاده‌سازی فعلی).
- در صورت خطا انتظار می‌رود پاسخ با `status: "error"` و پیام مناسب برگردد.
---
## ۲. API دریافت زمینه مزرعه (Farm Context)
برای پر کردن نوار «زمینه مزرعه» در بالای چت (نوع خاک، EC آب، محصول انتخاب‌شده، مرحله رشد، آخرین آبیاری).
- **وضعیت:** در بک‌اند فعلی endpoint اختصاصی برای «یک جا گرفتن» زمینه مزرعه وجود ندارد.
- **راه‌حل فعلی:** فرانت می‌تواند داده را از منابع موجود جمع کند:
- **توصیه آبیاری:** `GET /api/irrigation-recommendation/config/``farmInfo` (soilType, waterQuality, climateZone) و `cropOptions`
- **توصیه کوددهی:** `GET /api/fertilization-recommendation/config/``farmData` (soilType, organicMatter, waterEC)، `growthStages`، `cropOptions`
- **پیشنهاد برای آینده:** یک endpoint مثل `GET /api/farm-ai-assistant/context/` یا `GET /api/farm-dashboard-config/...` که یک آبجکت هم‌خوان با `FarmContext` فرانت برگرداند (مثلاً `soilType`, `waterEC`, `selectedCrop`, `growthStage`, `lastIrrigationStatus`).
---
## ۳. API توصیه آبیاری
وقتی کاربر در چت درخواست توصیه آبیاری می‌کند (یا دستیار تصمیم می‌گیرد توصیه آبیاری بدهد)، می‌توان از API موجود استفاده کرد.
- **Config (برای فرم/گزینه‌ها):** `GET /api/irrigation-recommendation/config/`
- **توصیه (برنامه آبیاری):** `POST /api/irrigation-recommendation/recommend/`
ورودی پیشنهادی برای `recommend`: `crop_id`, `soilType`, `waterQuality`, `climateZone` (در نسخه فعلی mock است و در پاسخ استفاده نمی‌شود).
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۱-توصیه-آبیاری-irrigation-recommendation).
---
## ۴. API توصیه کوددهی
در صورت درخواست کاربر برای توصیه کود یا تصمیم دستیار برای دادن توصیه کود.
- **Config:** `GET /api/fertilization-recommendation/config/`
- **توصیه:** `POST /api/fertilization-recommendation/recommend/`
ورودی پیشنهادی برای `recommend`: `crop_id`, `growth_stage`, `soilType`, `organicMatter`, `waterEC` (در نسخه فعلی mock است).
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۳-توصیه-کوددهی-fertilization-recommendation).
---
## ۵. API تشخیص آفت از تصویر
وقتی کاربر در چت تصویر گیاه را ارسال می‌کند (دکمه دوربین یا آپلود).
- **تحلیل تصویر:** `POST /api/pest-detection/analyze/`
- **ورودی:** بدن درخواست می‌تواند شامل تصویر (مثلاً form-data با فایل یا JSON با base64) باشد. در نسخه فعلی پاسخ ثابت (mock) است.
- **خروجی:** `pest`, `confidence`, `description`, `treatment`
جزئیات و نمونه پاسخ: [RECOMMENDATION_APIS.md](./RECOMMENDATION_APIS.md#۲-تشخیص-آفت-pest-detection).
---
## خلاصه Endpointها برای Farm AI Assistant
| ردیف | API | متد | Endpoint | وضعیت |
|------|-----|------|----------|--------|
| ۱ | Farm AI Chat | POST | `/api/farm-ai-assistant/chat/` | موجود |
| ۱.۱ | Farm AI Chat Task Create | POST | `/api/farm-ai-assistant/chat/task/` | موجود |
| ۱.۲ | Farm AI Chat Task Status | GET | `/api/farm-ai-assistant/chat/task/{task_id}/status/` | موجود |
| ۲ | Farm Context | GET | `/api/farm-ai-assistant/context/` (پیشنهادی) | **پیاده‌سازی نشده**؛ استفاده از configهای آبیاری/کوددهی |
| ۳ | توصیه آبیاری | GET | `/api/irrigation-recommendation/config/` | موجود (mock) |
| ۳ | توصیه آبیاری | POST | `/api/irrigation-recommendation/recommend/` | موجود (mock) |
| ۴ | توصیه کوددهی | GET | `/api/fertilization-recommendation/config/` | موجود (mock) |
| ۴ | توصیه کوددهی | POST | `/api/fertilization-recommendation/recommend/` | موجود (mock) |
| ۵ | تشخیص آفت | POST | `/api/pest-detection/analyze/` | موجود (mock) |
+546
View File
@@ -0,0 +1,546 @@
# مستند نحوه ارتباط با سرویس `farm_ai_assistant`
این فایل برای تیم بک‌اند تهیه شده و صرفاً بر اساس مسیرهای فعال در `farm_ai_assistant/urls.py` و رفتار فعلی ویوها نوشته شده است.
## Base URL
تمام endpointهای این سرویس با prefix زیر در دسترس هستند:
```text
/api/farm-ai-assistant/
```
نمونه کامل:
```text
https://<domain>/api/farm-ai-assistant/
```
## احراز هویت
تمام endpointهای عملیاتی این ماژول نیاز به احراز هویت دارند:
- نوع دسترسی: `IsAuthenticated`
- هدر موردنیاز:
```http
Authorization: Bearer <access_token>
```
> اگر کاربر لاگین نباشد، درخواست با خطای احراز هویت رد می‌شود.
## وضعیت endpointها
### endpointهای فعال در `farm_ai_assistant/urls.py`
| Method | Endpoint | کاربرد |
|---|---|---|
| `GET` | `/context/` | دریافت context نمایشی/پیش‌فرض برای UI |
| `POST` | `/chat/task/` | ثبت پیام کاربر و ساخت task در سرویس AI |
| `GET` | `/chat/task/<task_id>/status/` | بررسی وضعیت task و دریافت نتیجه نهایی |
| `GET` | `/chats/` | لیست گفتگوهای کاربر |
| `POST` | `/chats/` | ایجاد conversation خالی |
| `DELETE` | `/chats/<conversation_id>/` | حذف conversation |
| `GET` | `/chats/<conversation_id>/messages/` | دریافت پیام‌های یک conversation |
### endpoint غیرفعال
endpoint زیر در فایل `urls.py` کامنت شده و در حال حاضر قابل استفاده نیست:
```text
POST /api/farm-ai-assistant/chat/
```
یعنی در وضعیت فعلی، جریان اصلی ارتباط با AI به شکل **asynchronous task-based** پیاده‌سازی شده است.
---
## 1) دریافت context
### Request
```http
GET /api/farm-ai-assistant/context/
Authorization: Bearer <token>
```
### Response
```json
{
"status": "success",
"data": {
"...": "context mock/initial data"
}
}
```
### توضیح
- این endpoint فعلاً داده context را از `mock_data` برمی‌گرداند.
- برای preload کردن اطلاعات اولیه UI مناسب است.
- این endpoint به سرویس خارجی AI کال نمی‌زند.
---
## 2) ایجاد task برای ارسال پیام به AI
این endpoint مهم‌ترین مسیر ارتباطی با سرویس AI است.
### Request
```http
POST /api/farm-ai-assistant/chat/task/
Authorization: Bearer <token>
Content-Type: application/json
```
### Body
```json
{
"content": "برای گوجه در مرحله گلدهی برنامه آبیاری بده",
"images": [],
"conversation_id": "optional-uuid",
"title": "optional title",
"farm_context": {
"soilType": "Loamy",
"waterEC": "1.2 dS/m",
"selectedCrop": "Tomato",
"growthStage": "Flowering"
}
}
```
### فیلدها
| فیلد | نوع | اجباری | توضیح |
|---|---|---|---|
| `content` | `string` | اختیاری* | متن پیام کاربر |
| `images` | `string[]` | اختیاری | لیست URL/base64 تصویر |
| `conversation_id` | `uuid` | اختیاری | اگر گفتگو از قبل وجود دارد |
| `title` | `string` | اختیاری | عنوان گفتگو |
| `farm_context` | `object` | اختیاری | context مزرعه |
> حداقل یکی از `content` یا `images` باید ارسال شود. در غیر این صورت validation error برمی‌گردد.
### رفتار بک‌اند داخلی
در این endpoint این اتفاق‌ها می‌افتد:
1. اگر `conversation_id` ارسال شده باشد، همان conversation متعلق به همان کاربر لود می‌شود.
2. اگر `conversation_id` ارسال نشده باشد، یک conversation جدید ساخته می‌شود.
3. یک message با `role=user` در دیتابیس ذخیره می‌شود.
4. سپس بک‌اند به سرویس خارجی AI درخواست می‌زند.
### payload ارسالی به سرویس خارجی AI
بک‌اند این payload را به سرویس خارجی ارسال می‌کند:
```json
{
"content": "...",
"query": "...",
"images": [],
"conversation_id": "conversation-uuid",
"user_id": 123,
"farm_context": {},
"title": "..."
}
```
### مسیر سرویس خارجی AI
```text
POST /rag/chat/generate
```
### Response موفق
```json
{
"status": "success",
"data": {
"task_id": "abc123",
"status": "PENDING",
"status_url": "/tasks/abc123/status",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"message_id": "5d3f7a8c-9f2e-4d0a-b56f-1f2c2f9c1a22"
}
}
```
### کدهای وضعیت محتمل
| Status Code | معنی |
|---|---|
| `202` | task ساخته شده و باید polling انجام شود |
| `4xx/5xx` | خطای دریافتی از سرویس خارجی |
| `503` | سرویس خارجی AI در دسترس نیست |
---
## 3) بررسی وضعیت task
فرانت یا سرویس مصرف‌کننده باید با `task_id` وضعیت را poll کند.
### Request
```http
GET /api/farm-ai-assistant/chat/task/<task_id>/status/
Authorization: Bearer <token>
```
### مسیر سرویس خارجی AI
بک‌اند این درخواست را به سرویس خارجی پاس می‌دهد:
```text
GET /tasks/<task_id>/status
```
### Response در حالت در حال پردازش
```json
{
"status": "success",
"data": {
"task_id": "abc123",
"status": "PENDING",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"progress": {
"message": "Processing request"
}
}
}
```
### Response در حالت موفق
```json
{
"status": "success",
"data": {
"task_id": "abc123",
"status": "SUCCESS",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"result": {
"message_id": "9f3f8f61-cc71-4f70-a650-2f4dc6f4e5c2",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"content": "پیشنهاد آبیاری شما آماده است",
"sections": [
{
"type": "recommendation",
"title": "Irrigation recommendation",
"icon": "droplet",
"frequency": "3 times per week",
"amount": "1520 L per plant",
"timing": "Early morning",
"expandableExplanation": "..."
}
]
}
}
}
```
### Response در حالت خطا
```json
{
"status": "success",
"data": {
"task_id": "abc123",
"status": "FAILURE",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"error": "something went wrong"
}
}
```
### نکته مهم
اگر `status=SUCCESS` باشد و `result` از سرویس خارجی دریافت شود:
- نتیجه نهایی به message دستیار تبدیل و در دیتابیس ذخیره می‌شود.
- یک `assistant message` داخل همان conversation ساخته یا به‌روزرسانی می‌شود.
- خروجی `result` در پاسخ API به فرم نهایی UI normalize می‌شود.
---
## 4) لیست گفتگوها
### Request
```http
GET /api/farm-ai-assistant/chats/
Authorization: Bearer <token>
```
### Response
```json
{
"status": "success",
"data": [
{
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"message_count": 4
}
]
}
```
### توضیح
- فقط conversationهای متعلق به همان کاربر لاگین‌شده برگردانده می‌شود.
- مرتب‌سازی بر اساس `updated_at desc` و سپس `created_at desc` است.
---
## 5) ایجاد conversation خالی
### Request
```http
POST /api/farm-ai-assistant/chats/
Authorization: Bearer <token>
Content-Type: application/json
```
### Body
```json
{
"title": "مشاوره آبیاری",
"farm_context": {
"soilType": "Loamy"
}
}
```
### Response
```json
{
"status": "success",
"data": {
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"message_count": 0
}
}
```
### کد وضعیت
```text
201 Created
```
---
## 6) دریافت پیام‌های یک conversation
### Request
```http
GET /api/farm-ai-assistant/chats/<conversation_id>/messages/
Authorization: Bearer <token>
```
### Response
```json
{
"status": "success",
"data": {
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"messages": [
{
"message_id": "11111111-1111-1111-1111-111111111111",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"role": "user",
"content": "برای گوجه آبیاری پیشنهاد بده",
"sections": [],
"images": [],
"created_at": "2025-03-27T12:00:00Z"
},
{
"message_id": "22222222-2222-2222-2222-222222222222",
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
"role": "assistant",
"content": "",
"sections": [
{
"type": "list",
"title": "Key points",
"items": [
"Avoid midday watering",
"Use drip irrigation"
]
}
],
"images": [],
"created_at": "2025-03-27T12:00:05Z"
}
]
}
}
```
### توضیح
- `role` یکی از دو مقدار `user` یا `assistant` است.
- برای messageهای `assistant`، فیلد `sections` می‌تواند پر باشد.
- برای messageهای `user`، معمولاً `sections` خالی است.
---
## 7) حذف conversation
### Request
```http
DELETE /api/farm-ai-assistant/chats/<conversation_id>/
Authorization: Bearer <token>
```
### Response
```json
{
"status": "success",
"data": {
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1"
}
}
```
---
## فرمت `sections` در پاسخ نهایی AI
بخش `sections` برای render شدن خروجی دستیار در UI استفاده می‌شود.
### ساختار هر section
```json
{
"type": "recommendation",
"title": "Irrigation recommendation",
"content": "",
"items": [],
"icon": "droplet",
"frequency": "3 times per week",
"amount": "1520 L per plant",
"timing": "Early morning",
"expandableExplanation": "..."
}
```
### مقادیر مجاز `type`
- `text`
- `list`
- `recommendation`
- `warning`
### نکات نرمال‌سازی
بک‌اند فقط این کلیدها را از پاسخ AI نگه می‌دارد:
- `type`
- `title`
- `content`
- `items`
- `icon`
- `frequency`
- `amount`
- `timing`
- `expandableExplanation`
اگر سرویس خارجی فیلد اضافی برگرداند، در response نهایی حذف می‌شود.
---
## قواعد مهم کسب‌وکاری/فنی
### مالکیت conversation
- هر conversation فقط برای owner خودش قابل مشاهده/حذف/دریافت پیام است.
- اگر `conversation_id` مربوط به کاربر دیگری باشد، `404 Conversation not found` برمی‌گردد.
### title conversation
- اگر conversation جدید بدون title ساخته شود:
- در endpoint `POST /chats/` عنوان پیش‌فرض `New chat` است.
- در endpoint `POST /chat/task/` اگر title داده نشود، از ابتدای `content` یا fallback پیش‌فرض استفاده می‌شود.
### ذخیره‌سازی پیام‌ها
- پیام کاربر قبل از تماس با AI ذخیره می‌شود.
- پاسخ دستیار بعد از اتمام task ذخیره می‌شود.
- داده خام/کمکی در `raw_response` نگهداری می‌شود.
### تصاویر
- فیلد `images` آرایه‌ای از string است.
- این string می‌تواند URL، path یا base64 باشد؛ validation فعلی فقط نوع string را چک می‌کند.
---
## فلو پیشنهادی برای استفاده از API
### سناریوی استاندارد چت
1. فرانت در صورت نیاز `GET /context/` را صدا می‌زند.
2. فرانت پیام کاربر را با `POST /chat/task/` ارسال می‌کند.
3. از پاسخ، `task_id` و `conversation_id` دریافت می‌شود.
4. فرانت هر چند ثانیه `GET /chat/task/<task_id>/status/` را poll می‌کند.
5. وقتی `status=SUCCESS` شد، نتیجه نهایی از `result` خوانده می‌شود.
6. برای history کامل، `GET /chats/<conversation_id>/messages/` فراخوانی می‌شود.
---
## نمونه cURL
### ارسال پیام و ساخت task
```bash
curl -X POST 'https://<domain>/api/farm-ai-assistant/chat/task/' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"content": "برای خیار گلخانه‌ای برنامه آبیاری بده",
"farm_context": {
"soilType": "Sandy Loam",
"selectedCrop": "Cucumber",
"growthStage": "Flowering"
}
}'
```
### بررسی وضعیت task
```bash
curl -X GET 'https://<domain>/api/farm-ai-assistant/chat/task/abc123/status/' \
-H 'Authorization: Bearer <token>'
```
### دریافت پیام‌های گفتگو
```bash
curl -X GET 'https://<domain>/api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/messages/' \
-H 'Authorization: Bearer <token>'
```
---
## جمع‌بندی سریع برای تیم backend
- endpoint sync chat فعلاً فعال نیست.
- endpoint اصلی برای ارتباط با AI برابر `POST /chat/task/` است.
- نتیجه از طریق `GET /chat/task/<task_id>/status/` گرفته می‌شود.
- conversation و messageها داخل دیتابیس داخلی ذخیره می‌شوند.
- همه endpointها نیازمند authentication هستند.
- ساختار خروجی نهایی باید با `sections` برای UI سازگار باشد.
-357
View File
@@ -1,357 +0,0 @@
# مستند ارتباط فرانت با API تنظیمات داشبورد فارم
این فایل مشخص می‌کند فرانت‌اند برای endpoint زیر چه request و responseی انتظار دارد:
```text
http://localhost:8000/api/farm-dashboard-config
```
این endpoint در فرانت از طریق فایل `src/libs/api/services/farmDashboardService.ts` مصرف می‌شود.
---
## خلاصه رفتار فرانت
- فرانت برای دریافت تنظیمات از `GET /api/farm-dashboard-config` استفاده می‌کند.
- فرانت برای ذخیره تغییرات از `PATCH /api/farm-dashboard-config` استفاده می‌کند.
- در `PATCH` فقط فیلدهای تغییرکرده ارسال می‌شوند.
- اما در response بهتر است همیشه **کل تنظیمات نهایی** برگردانده شود.
- فرانت response را هم در حالت wrapper شده و هم بدون wrapper می‌پذیرد.
---
## فرمت response قابل قبول
فرانت هر دو فرمت زیر را می‌پذیرد.
### فرمت پیشنهادی
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
}
```
### فرمت قابل قبول بدون wrapper
```json
{
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
```
---
## GET
### Request
```http
GET /api/farm-dashboard-config
Content-Type: application/json
Authorization: Bearer <token>
```
> هدر `Authorization` فقط وقتی ارسال می‌شود که توکن در `localStorage` موجود باشد.
### Response مورد انتظار
فرانت در خروجی GET این ساختار را انتظار دارد:
| فیلد | نوع | توضیح |
|---|---|---|
| `disabled_card_ids` | `string[]` | لیست کارت‌های مخفی‌شده |
| `row_order` | `string[]` | ترتیب ردیف‌های داشبورد |
| `enable_drag_reorder` | `boolean` | فعال/غیرفعال بودن drag reorder |
### مثال response
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
}
```
### نکات مهم GET
- اگر `enable_drag_reorder` برنگردد، فرانت مقدار پیش‌فرض `true` در نظر می‌گیرد.
- اگر `row_order` نامعتبر یا خالی باشد، فرانت از ترتیب پیش‌فرض خودش استفاده می‌کند.
- اگر request خطا بخورد، فرانت اول `localStorage` را چک می‌کند؛ اگر چیزی نبود، config پیش‌فرض را می‌سازد.
---
## PATCH
### رفتار request
فرانت فقط فیلدهای تغییرکرده را می‌فرستد. یعنی body می‌تواند یکی از حالت‌های زیر باشد یا ترکیبی از آن‌ها:
#### 1) تغییر کارت‌های غیرفعال
```json
{
"disabled_card_ids": ["farmWeatherCard", "sensorRadarChart"]
}
```
#### 2) تغییر ترتیب ردیف‌ها
```json
{
"row_order": [
"overviewKpis",
"weatherAlerts",
"predictions",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"soilHeatmap",
"ndviRecommendations",
"economic"
]
}
```
#### 3) فعال/غیرفعال کردن drag reorder
```json
{
"enable_drag_reorder": false
}
```
#### 4) ترکیبی
```json
{
"disabled_card_ids": ["farmWeatherCard"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
```
### Response مورد انتظار برای PATCH
هرچند request می‌تواند partial باشد، ولی response بهتر است همیشه **کل state نهایی config** را برگرداند:
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": ["farmWeatherCard"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
}
```
### نکته خیلی مهم برای PATCH
در پیاده‌سازی فعلی فرانت، response باید حداقل یکی از این دو فیلد را داشته باشد:
- `disabled_card_ids`
- `row_order`
اگر backend فقط این را برگرداند:
```json
{
"enable_drag_reorder": false
}
```
ممکن است فرانت این response را نامعتبر تشخیص دهد.
بنابراین برای جلوگیری از مشکل، **همیشه کل object نهایی config را برگردانید**.
---
## mapping بین فرانت و API
فرانت state داخلی را با نام‌های camelCase نگه می‌دارد، اما در request به snake_case تبدیل می‌کند:
| Frontend field | API field |
|---|---|
| `disabledCardIds` | `disabled_card_ids` |
| `rowOrder` | `row_order` |
| `enableDragReorder` | `enable_drag_reorder` |
---
## مقادیر معتبر پیشنهادی
### Row ID های معتبر
```json
[
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
]
```
### Card ID های معتبر
```json
[
"farmOverviewKpis",
"farmWeatherCard",
"farmAlertsTracker",
"sensorValuesList",
"sensorRadarChart",
"sensorComparisonChart",
"anomalyDetectionCard",
"farmAlertsTimeline",
"waterNeedPrediction",
"harvestPredictionCard",
"yieldPredictionChart",
"soilMoistureHeatmap",
"ndviHealthCard",
"recommendationsList",
"economicOverview"
]
```
---
## فرمت canonical پیشنهادی برای backend
اگر بخواهی backend کاملاً سازگار و بدون ambiguity باشد، بهترین قرارداد این است:
- `disabled_card_ids` فقط شامل `Card ID` باشد
- `row_order` فقط شامل `Row ID` باشد
- response همیشه full object برگرداند
- status موفق `200` باشد
- `Content-Type` برابر `application/json` باشد
---
## نمونه نهایی پیشنهادی
### GET success
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": [],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": true
}
}
```
### PATCH success
```json
{
"code": 200,
"msg": "OK",
"data": {
"disabled_card_ids": ["farmWeatherCard"],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic"
],
"enable_drag_reorder": false
}
}
```
---
## منبع این مستند
این مستند بر اساس رفتار واقعی فرانت در فایل‌های زیر نوشته شده است:
- `src/libs/api/services/farmDashboardService.ts`
- `src/views/dashboards/farm/farmDashboardConfig.ts`
- `src/views/dashboards/farm/FarmDashboardWrapper.tsx`
-439
View File
@@ -1,439 +0,0 @@
# مستند پاسخ API برای فیچرهای Farm AI
این فایل، پاسخ‌های API موردنیاز برای سه فیچر زیر را یک‌جا جمع می‌کند:
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
> مبنا، پیاده‌سازی فعلی فرانت در `src/views/dashboards/farm/...` و سرویس‌های `src/libs/api/services/...` است.
---
## قرارداد کلی پاسخ‌ها
در هر سه سرویس، فرانت انتظار این wrapper را دارد:
```json
{
"status": "success",
"data": {}
}
```
- `status`: معمولاً `success` یا `error`
- `data`: payload اصلی که فرانت unwrap می‌کند
---
## 1) Farm AI Assistant
### صفحه
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
- کامپوننت اصلی: `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
### APIهای موردنیاز
1. `GET /api/farm-ai-assistant/context/`
2. `POST /api/farm-ai-assistant/chat/`
---
### 1.1) دریافت context مزرعه
#### Endpoint
`GET /api/farm-ai-assistant/context/`
#### Response
```json
{
"status": "success",
"data": {
"soilType": "Loamy",
"waterEC": "1.2 dS/m",
"selectedCrop": "Tomato",
"growthStage": "Flowering",
"lastIrrigationStatus": "2 days ago"
}
}
```
#### فیلدهای لازم
| فیلد | نوع | مصرف در UI |
|------|-----|------------|
| `soilType` | `string` | badge نوع خاک |
| `waterEC` | `string` | badge EC آب |
| `selectedCrop` | `string` | badge محصول انتخاب‌شده |
| `growthStage` | `string` | badge مرحله رشد |
| `lastIrrigationStatus` | `string` | badge آخرین وضعیت آبیاری |
#### نکته
اگر این API خطا بدهد، فرانت fallback داخلی دارد و toast خطا نمایش می‌دهد.
---
### 1.2) ارسال پیام به دستیار
#### Endpoint
`POST /api/farm-ai-assistant/chat/`
#### Request body
```json
{
"content": "What is the best irrigation plan for tomato?",
"farm_context": {
"soilType": "Loamy",
"waterEC": "1.2 dS/m",
"selectedCrop": "Tomato",
"growthStage": "Flowering",
"lastIrrigationStatus": "2 days ago"
},
"conversation_id": "conv-123"
}
```
#### Response
```json
{
"status": "success",
"data": {
"message_id": "msg-001",
"conversation_id": "conv-123",
"content": "Here is the recommended plan.",
"sections": [
{
"type": "recommendation",
"title": "Irrigation Plan",
"icon": "droplet",
"frequency": "3 times per week",
"amount": "15 liters per plant",
"timing": "Early morning",
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough."
},
{
"type": "list",
"title": "Important Notes",
"icon": "leaf",
"items": [
"Avoid watering at noon",
"Check leaf stress every two days"
]
},
{
"type": "warning",
"title": "Heat Alert",
"icon": "warning",
"content": "Increase irrigation if temperature rises above 35°C."
}
]
}
}
```
#### فیلدهای لازم در response
| فیلد | نوع | توضیح |
|------|-----|-------|
| `message_id` | `string` | id پیام assistant |
| `conversation_id` | `string` | برای ادامه چت در پیام‌های بعدی |
| `content` | `string` | متن ساده پاسخ |
| `sections` | `ChatSection[]` | خروجی ساخت‌یافته برای رندر کارت‌ها |
#### ساختار `ChatSection`
| فیلد | نوع | اجباری | توضیح |
|------|-----|--------|-------|
| `type` | `'text' \| 'list' \| 'recommendation' \| 'warning'` | بله | نوع سکشن |
| `title` | `string` | خیر | عنوان سکشن |
| `content` | `string` | خیر | متن سکشن |
| `items` | `string[]` | خیر | برای لیست |
| `icon` | `'droplet' \| 'leaf' \| 'warning' \| 'fertilizer' \| 'calendar'` | خیر | آیکون نمایشی |
| `frequency` | `string` | خیر | فقط برای `recommendation` |
| `amount` | `string` | خیر | فقط برای `recommendation` |
| `timing` | `string` | خیر | فقط برای `recommendation` |
| `expandableExplanation` | `string` | خیر | توضیح بازشونده |
#### حداقل توصیه
- `sections` همیشه به صورت آرایه برگردد، حتی اگر خالی باشد.
- `conversation_id` بعد از اولین پاسخ حتماً برگردد.
- اگر پاسخ فقط structured است، `content` می‌تواند رشته خالی باشد.
---
## 2) Fertilization Recommendation
### صفحه
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
- کامپوننت اصلی: `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
### APIهای موردنیاز
1. `GET /api/fertilization-recommendation/config/`
2. `POST /api/fertilization-recommendation/recommend/`
---
### 2.1) دریافت config اولیه
#### Endpoint
`GET /api/fertilization-recommendation/config/`
#### Response
```json
{
"status": "success",
"data": {
"farmData": {
"soilType": "Loamy",
"organicMatter": "Medium (2.5%)",
"waterEC": "1.2 dS/m"
},
"growthStages": [
{ "id": "prePlanting", "icon": "tabler-seedling" },
{ "id": "earlyGrowth", "icon": "tabler-leaf" },
{ "id": "flowering", "icon": "tabler-flower" },
{ "id": "fruiting", "icon": "tabler-apple" },
{ "id": "postHarvest", "icon": "tabler-basket" }
],
"cropOptions": [
{ "id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat" },
{ "id": "corn", "labelKey": "corn", "icon": "tabler-plant-2" }
]
}
}
```
#### فیلدهای لازم
##### `farmData`
| فیلد | نوع |
|------|-----|
| `soilType` | `string` |
| `organicMatter` | `string` |
| `waterEC` | `string` |
##### `growthStages[]`
| فیلد | نوع | توضیح |
|------|-----|-------|
| `id` | `string` | id مرحله رشد |
| `icon` | `string` | نام آیکون |
##### `cropOptions[]`
| فیلد | نوع | توضیح |
|------|-----|-------|
| `id` | `string` | id محصول برای submit |
| `labelKey` | `string` | کلید ترجمه |
| `icon` | `string` | نام آیکون |
#### نکته
- اگر `growthStages` مقدار داشته باشد، اولین آیتم به عنوان پیش‌فرض انتخاب می‌شود.
- اگر `cropOptions` خالی باشد، لیست انتخاب محصول خالی نمایش داده می‌شود.
---
### 2.2) دریافت برنامه کوددهی
#### Endpoint
`POST /api/fertilization-recommendation/recommend/`
#### Request body
```json
{
"crop_id": "wheat",
"growth_stage": "flowering",
"soilType": "Loamy",
"organicMatter": "Medium (2.5%)",
"waterEC": "1.2 dS/m"
}
```
#### Response
```json
{
"status": "success",
"data": {
"plan": {
"npkRatio": "20-20-20",
"amountPerHectare": "150 kg/ha",
"applicationMethod": "Fertigation",
"applicationInterval": "Every 10 days",
"reasoning": "Balanced NPK is recommended during flowering to support bloom and fruit set."
}
}
}
```
#### فیلدهای لازم در `plan`
| فیلد | نوع | مصرف در UI |
|------|-----|------------|
| `npkRatio` | `string` | نوع/نسبت کود |
| `amountPerHectare` | `string` | مقدار مصرف |
| `applicationMethod` | `string` | روش مصرف |
| `applicationInterval` | `string` | بازه تکرار |
| `reasoning` | `string` | توضیح بازشونده |
#### نکته
فرانت مستقیماً `data.plan` را انتظار دارد. اگر `plan` نباشد، نتیجه‌ای نمایش داده نمی‌شود.
---
## 3) Irrigation Recommendation
### صفحه
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
- کامپوننت اصلی: `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
### APIهای موردنیاز
1. `GET /api/irrigation-recommendation/config/`
2. `POST /api/irrigation-recommendation/recommend/`
---
### 3.1) دریافت config اولیه
#### Endpoint
`GET /api/irrigation-recommendation/config/`
#### Response
```json
{
"status": "success",
"data": {
"farmInfo": {
"soilType": "Loamy",
"waterQuality": "Medium EC",
"climateZone": "Temperate"
},
"cropOptions": [
{ "id": "tomato", "labelKey": "tomato", "icon": "tabler-plant-2" },
{ "id": "cucumber", "labelKey": "cucumber", "icon": "tabler-leaf" }
]
}
}
```
#### فیلدهای لازم
##### `farmInfo`
| فیلد | نوع |
|------|-----|
| `soilType` | `string` |
| `waterQuality` | `string` |
| `climateZone` | `string` |
##### `cropOptions[]`
| فیلد | نوع | توضیح |
|------|-----|-------|
| `id` | `string` | id محصول برای submit |
| `labelKey` | `string` | کلید ترجمه |
| `icon` | `string` | نام آیکون |
#### نکته
در این صفحه `farmInfo` بدون چک null مستقیماً set می‌شود؛ بهتر است API همیشه این آبجکت را برگرداند.
---
### 3.2) دریافت برنامه آبیاری
#### Endpoint
`POST /api/irrigation-recommendation/recommend/`
#### Request body
```json
{
"crop_id": "tomato"
}
```
> در سرویس، فیلدهای `soilType`, `waterQuality`, `climateZone` هم پشتیبانی شده‌اند، ولی در UI فعلی فقط `crop_id` ارسال می‌شود.
#### Response
```json
{
"status": "success",
"data": {
"plan": {
"frequencyPerWeek": 3,
"durationMinutes": 25,
"bestTimeOfDay": "Early morning",
"moistureLevel": 68,
"warning": "Reduce irrigation if rainfall occurs this week."
}
}
}
```
#### فیلدهای لازم در `plan`
| فیلد | نوع | مصرف در UI |
|------|-----|------------|
| `frequencyPerWeek` | `number` | تعداد دفعات در هفته |
| `durationMinutes` | `number` | مدت هر نوبت |
| `bestTimeOfDay` | `string` | زمان مناسب آبیاری |
| `moistureLevel` | `number` | درصد moisture برای دایره progress |
| `warning` | `string` | هشدار اختیاری |
#### محدودیت مهم
- `moistureLevel` بهتر است بین `0` تا `100` باشد، چون مستقیم برای محاسبه progress circle استفاده می‌شود.
---
## جمع‌بندی سریع برای بک‌اند
### Farm AI Assistant
- `GET /api/farm-ai-assistant/context/``data: FarmContext`
- `POST /api/farm-ai-assistant/chat/``data: { message_id, conversation_id, content, sections }`
### Fertilization Recommendation
- `GET /api/fertilization-recommendation/config/``data: { farmData, growthStages, cropOptions }`
- `POST /api/fertilization-recommendation/recommend/``data: { plan }`
### Irrigation Recommendation
- `GET /api/irrigation-recommendation/config/``data: { farmInfo, cropOptions }`
- `POST /api/irrigation-recommendation/recommend/``data: { plan }`
---
## مسیرهای مرجع کد
- `src/libs/api/services/farmAiAssistantService.ts`
- `src/libs/api/services/fertilizationRecommendationService.ts`
- `src/libs/api/services/irrigationRecommendationService.ts`
- `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
- `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
- `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
-465
View File
@@ -1,465 +0,0 @@
# قرارداد API شبیه‌ساز گیاه (Plant Simulator)
این سند تمام داده‌هایی را که باید **از بکند بیاید**، **به بکند فرستاده شود** و دوباره **از بکند برگردد** به‌همراه ساختار و داده‌های ماک توصیف می‌کند.
---
## ۱. داده‌های اولیه (از بکند → فرانت)
این داده‌ها یک‌بار هنگام لود صفحه (یا هنگام باز کردن شبیه‌ساز) از بکند گرفته می‌شوند.
### ۱.۱ تنظیمات اسلایدرها (کنترل‌های محیطی)
هر اسلایدر (نور، آب، pH خاک، سرعت رشد و غیره) باید از بکند **حداقل، حداکثر، گام، واحد و برچسب** بگیرد. واحد می‌تواند درصد (`percent`) یا عدد با واحد متنی (`number` + `unit`) باشد.
```ts
// GET /api/plant-simulator/config یا بخشی از همین endpoint
interface SliderConfig {
key: string // مثلاً: "light" | "water" | "soil_ph" | "growth_speed"
label: string // برچسب نمایشی (مثلاً "نور"، "آب")
min: number
max: number
step: number
unit_type: 'percent' | 'number'
unit?: string // وقتی unit_type === 'number' مثلاً "g/s", "ppm", "°C"
default_value: number
icon?: string // اختیاری: مثلاً "☀️", "💧"
}
// پاسخ نمونه
{
"sliders": [
{
"key": "light",
"label": "نور",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 75,
"icon": "☀️"
},
{
"key": "water",
"label": "آب",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 65,
"icon": "💧"
},
{
"key": "soil_ph",
"label": "pH خاک",
"min": 4,
"max": 9,
"step": 0.5,
"unit_type": "number",
"unit": "",
"default_value": 6.5
},
{
"key": "growth_speed",
"label": "سرعت رشد",
"min": 0.5,
"max": 5,
"step": 0.5,
"unit_type": "number",
"unit": "×",
"default_value": 1.5
}
]
}
```
### ۱.۲ ثابت‌های شبیه‌ساز (حداکثرها و محدوده چارت)
برای نمایش صحیح ارتفاع، برگ، شاخه، میوه، محصول و محورهای چارت باید از بکند بیاید.
```ts
// همان config یا GET /api/plant-simulator/constants
interface SimulatorConstants {
max_height: number // مثلاً 280 (px یا واحد ارتفاع)
max_leaves: number // مثلاً 14
max_branches: number // مثلاً 6
max_yield: number // مثلاً 500 (گرم)
yield_unit: string // مثلاً "g"
yield_rate_unit: string // مثلاً "g/s"
height_unit?: string // مثلاً "px" یا "cm"
}
// پاسخ نمونه
{
"max_height": 280,
"max_leaves": 14,
"max_branches": 6,
"max_yield": 500,
"yield_unit": "g",
"yield_rate_unit": "g/s",
"height_unit": "px"
}
```
### ۱.۳ تنظیمات چارت (محورها و سری‌ها)
عنوان چارت، برچسب محورها و محدوده min/max هر محور تا در فرانت به‌درستی رسم شود.
```ts
interface ChartConfig {
title: string
x_axis_label?: string // مثلاً "زمان (ثانیه)"
series: {
key: string // "height" | "leaves" | "yield" | "yield_rate"
label: string
y_axis_id: string // "yHeight" | "yLeaf" | "yYield" | "yYieldRate"
min: number
max: number
unit?: string
}[]
}
// پاسخ نمونه
{
"chart": {
"title": "پیشرفت رشد",
"x_axis_label": "زمان (ثانیه)",
"series": [
{ "key": "height", "label": "ارتفاع (px)", "y_axis_id": "yHeight", "min": 0, "max": 280, "unit": "px" },
{ "key": "leaves", "label": "تعداد برگ", "y_axis_id": "yLeaf", "min": 0, "max": 14 },
{ "key": "yield", "label": "محصول (g)", "y_axis_id": "yYield", "min": 0, "max": 500, "unit": "g" },
{ "key": "yield_rate", "label": "نرخ محصول (g/s)", "y_axis_id": "yYieldRate", "min": 0, "unit": "g/s" }
]
}
}
```
---
## ۲. داده‌هایی که به بکند فرستاده می‌شوند
### ۲.۱ شروع شبیه‌سازی
وقتی کاربر دکمه «شروع» را می‌زند، مقادیر فعلی محیط و سرعت به بکند ارسال می‌شود.
```ts
// POST /api/plant-simulator/start
interface StartSimulationRequest {
environment: Record<string, number> // key مطابق slider.key ها
growth_speed: number
}
// مثال
{
"environment": {
"light": 75,
"water": 65,
"soil_ph": 6.5
},
"growth_speed": 1.5
}
```
### ۲.۲ توقف شبیه‌سازی
```ts
// POST /api/plant-simulator/stop
// بدنه خالی یا فقط session_id در صورت نیاز
{}
```
### ۲.۳ ریست
```ts
// POST /api/plant-simulator/reset
// بدنه خالی یا session_id
{}
```
### ۲.۴ به‌روزرسانی محیط (تغییر اسلایدرها)
هر بار کاربر نور، آب، pH یا سرعت را عوض کند، در حالت real-time می‌توان این را به بکند فرستاد (اختیاری؛ بکند می‌تواند فقط با start این مقادیر را بگیرد).
```ts
// PATCH /api/plant-simulator/environment
interface UpdateEnvironmentRequest {
environment: Record<string, number>
growth_speed?: number
}
// مثال
{
"environment": { "light": 80, "water": 70, "soil_ph": 6.5 },
"growth_speed": 2
}
```
---
## ۳. داده‌هایی که از بکند برمی‌گردند (حین / بعد شبیه‌سازی)
این داده‌ها یا با **polling** (مثلاً هر ۱ ثانیه) یا با **WebSocket/SSE** از بکند گرفته می‌شوند و در UI و چارت به‌صورت تدریجی به‌روز می‌شوند (مثلاً آرایه‌های چارت کم‌کم طولانی می‌شوند).
### ۳.۱ وضعیت گیاه (آمار کارت بالا)
تعداد برگ، شاخه، میوه، ارتفاع، محصول و نرخ محصول باید از بکند بیاید تا با سرعت رشد واقعی همگام باشد.
```ts
// GET /api/plant-simulator/state یا از طریق WebSocket
interface PlantStateResponse {
height: number
leaves_count: number
branches_count: number
fruits_count: number
yield: number
yield_rate: number
tick: number
is_healthy: boolean // آیا گیاه می‌تواند به سلامت به رشد ادامه دهد؟
can_continue: boolean // معادل is_healthy یا منطق جدا (مثلاً رسیدن به حداکثر ارتفاع)
}
// پاسخ نمونه (یک لحظه از زمان)
{
"height": 120,
"leaves_count": 4,
"branches_count": 2,
"fruits_count": 0,
"yield": 0,
"yield_rate": 0.012,
"tick": 340,
"is_healthy": true,
"can_continue": true
}
```
### ۳.۲ پیشرفت و وضعیت نوارهای پیشرفت
پیشرفت رشد، وضعیت نور، وضعیت آب و محصول‌دهی برای نوارهای پیشرفت (Progress) و نمایش درصد/عدد.
```ts
interface ProgressResponse {
growth_progress: number // 0..100 (بر اساس height / max_height)
light_status: number // مقدار فعلی نور (مثلاً 0..100 درصد)
water_status: number // مقدار فعلی آب (مثلاً 0..100 درصد)
yield_progress: number // 0..100 (بر اساس yield / max_yield)
yield_current: number
yield_rate_current: number
}
// پاسخ نمونه
{
"growth_progress": 42,
"light_status": 75,
"water_status": 65,
"yield_progress": 8,
"yield_current": 42.5,
"yield_rate_current": 0.093
}
```
این فیلدها می‌توانند داخل همان `PlantStateResponse` یا در یک endpoint جدا برگردانده شوند.
### ۳.۳ داده‌های چارت (تاریخچهٔ زمانی)
داده‌های چارت به‌صورت آرایه‌هایی هستند که **بر اساس زمان (مثلاً هر ثانیه) از بکند پر می‌شوند**. فرانت این آرایه‌ها را مستقیم به نمودار می‌دهد؛ هر نقطهٔ جدید با سرعت رشد گیاه اضافه می‌شود.
مثال: اگر بکند هر ثانیه یک بار state بفرستد، آرایه‌ها به این شکل طول می‌کشند:
- ثانیه ۰: `[0]`
- ثانیه ۱: `[0, 5]`
- ثانیه ۲: `[0, 5, 10]`
- ثانیه ۳: `[0, 5, 10, 30]`
- ثانیه ۴: `[0, 5, 10, 30, 40]`
یعنی **مقادیر با سرعت رشد گیاه کم‌کم به آرایه اضافه می‌شوند**.
```ts
// GET /api/plant-simulator/state یا بخشی از همان پاسخ
interface ChartHistoryResponse {
labels: string[] // مثلاً ["0s", "1s", "2s", ...]
height_history: number[]
leaf_history: number[]
yield_history: number[]
yield_rate_history: number[]
}
// پاسخ نمونه (بعد از چند ثانیه شبیه‌سازی)
{
"labels": ["0s", "1s", "2s", "3s", "4s", "5s"],
"height_history": [0, 5, 12, 28, 45, 68],
"leaf_history": [0, 0, 1, 2, 3, 4],
"yield_history": [0, 0, 0, 0.1, 0.5, 1.2],
"yield_rate_history": [0, 0, 0, 0.01, 0.03, 0.06]
}
```
- **ارتفاع (height_history)**: مقدار ارتفاع در هر نمونه (مثلاً هر ثانیه).
- **برگ (leaf_history)**: تعداد برگ در هر نمونه.
- **محصول (yield_history)**: مقدار تجمعی محصول (g) در هر نمونه.
- **نرخ محصول (yield_rate_history)**: نرخ لحظه‌ای (g/s) در هر نمونه.
محتوای داخل چارت دقیقاً همین آرایه‌هاست؛ فرانت فقط این‌ها را روی محور زمان رسم می‌کند و با هر پاسخ جدید یک (یا چند) نقطه به انتهای آرایه اضافه می‌شود.
---
## ۴. خلاصهٔ جریان داده
| جهت | زمان | داده |
|-----|------|------|
| بکند → فرانت | لود صفحه | Config: اسلایدرها (min, max, step, unit)، ثابت‌ها (max_height, max_leaves, ...)، تنظیمات چارت |
| فرانت → بکند | کلیک شروع | محیط (light, water, soil_ph, ...) و growth_speed |
| فرانت → بکند | کلیک توقف / ریست | stop یا reset |
| فرانت → بکند | (اختیاری) تغییر اسلایدر | environment + growth_speed |
| بکند → فرانت | حین شبیه‌سازی (polling/WS) | PlantState (height, leaves_count, branches_count, fruits_count, yield, yield_rate, is_healthy, can_continue) |
| بکند → فرانت | همان پاسخ | Progress (growth_progress, light_status, water_status, yield_progress, yield_current, yield_rate_current) |
| بکند → فرانت | همان پاسخ | ChartHistory (labels, height_history, leaf_history, yield_history, yield_rate_history) |
---
## ۵. داده‌های ماک (Mock) کامل
برای توسعه و تست فرانت بدون بکند می‌توان از این ماک استفاده کرد.
### ۵.۱ ماک Config (اسلایدرها + ثابت‌ها + چارت)
```json
{
"sliders": [
{
"key": "light",
"label": "نور",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 75,
"icon": "☀️"
},
{
"key": "water",
"label": "آب",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 65,
"icon": "💧"
},
{
"key": "soil_ph",
"label": "pH خاک",
"min": 4,
"max": 9,
"step": 0.5,
"unit_type": "number",
"unit": "",
"default_value": 6.5
},
{
"key": "growth_speed",
"label": "سرعت رشد",
"min": 0.5,
"max": 5,
"step": 0.5,
"unit_type": "number",
"unit": "×",
"default_value": 1.5
}
],
"constants": {
"max_height": 280,
"max_leaves": 14,
"max_branches": 6,
"max_yield": 500,
"yield_unit": "g",
"yield_rate_unit": "g/s",
"height_unit": "px"
},
"chart": {
"title": "پیشرفت رشد",
"x_axis_label": "زمان (ثانیه)",
"series": [
{ "key": "height", "label": "ارتفاع (px)", "y_axis_id": "yHeight", "min": 0, "max": 280, "unit": "px" },
{ "key": "leaves", "label": "تعداد برگ", "y_axis_id": "yLeaf", "min": 0, "max": 14 },
{ "key": "yield", "label": "محصول (g)", "y_axis_id": "yYield", "min": 0, "max": 500, "unit": "g" },
{ "key": "yield_rate", "label": "نرخ محصول (g/s)", "y_axis_id": "yYieldRate", "min": 0, "unit": "g/s" }
]
}
}
```
### ۵.۲ ماک State + Progress + Chart (یک پاسخ ترکیبی)
```json
{
"plant": {
"height": 142,
"leaves_count": 5,
"branches_count": 2,
"fruits_count": 0,
"yield": 12.4,
"yield_rate": 0.087,
"tick": 520,
"is_healthy": true,
"can_continue": true
},
"progress": {
"growth_progress": 50,
"light_status": 75,
"water_status": 65,
"yield_progress": 2.5,
"yield_current": 12.4,
"yield_rate_current": 0.087
},
"chart": {
"labels": ["0s", "1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s", "10s"],
"height_history": [0, 5, 12, 28, 45, 68, 92, 110, 125, 135, 142],
"leaf_history": [0, 0, 1, 2, 3, 4, 4, 5, 5, 5, 5],
"yield_history": [0, 0, 0, 0.1, 0.5, 1.2, 3.2, 5.8, 8.2, 10.1, 12.4],
"yield_rate_history": [0, 0, 0, 0.01, 0.03, 0.05, 0.06, 0.07, 0.08, 0.085, 0.087]
}
}
```
### ۵.۳ ماک برای نمایش تدریجی چارت
چارت باید بر اساس همین آرایه‌ها به‌صورت تدریجی پر شود؛ هر بار بکند state جدید می‌فرستد، یک نقطه به انتهای هر آرایه اضافه می‌شود. مثال برای چند لحظهٔ متوالی:
```json
// t=0
{ "labels": ["0s"], "height_history": [0], "leaf_history": [0], "yield_history": [0], "yield_rate_history": [0] }
// t=1s
{ "labels": ["0s", "1s"], "height_history": [0, 5], "leaf_history": [0, 0], "yield_history": [0, 0], "yield_rate_history": [0, 0] }
// t=2s
{ "labels": ["0s", "1s", "2s"], "height_history": [0, 5, 12], "leaf_history": [0, 0, 1], "yield_history": [0, 0, 0], "yield_rate_history": [0, 0, 0] }
// t=3s
{ "labels": ["0s", "1s", "2s", "3s"], "height_history": [0, 5, 12, 30], "leaf_history": [0, 0, 1, 2], "yield_history": [0, 0, 0, 0.1], "yield_rate_history": [0, 0, 0, 0.01] }
// t=4s
{ "labels": ["0s", "1s", "2s", "3s", "4s"], "height_history": [0, 5, 12, 30, 48], "leaf_history": [0, 0, 1, 2, 3], "yield_history": [0, 0, 0, 0.1, 0.5], "yield_rate_history": [0, 0, 0, 0.01, 0.03] }
```
ارتفاع و بقیهٔ مقادیر باید متناسب با سرعت رشد و منطق بکند افزایش یابند؛ اعداد بالا فقط نمونهٔ ماک هستند.
---
## ۶. نکات پیاده‌سازی فرانت
- **اسلایدرها**: با استفاده از `sliders` از config، هر کنترل با `min`, `max`, `step`, `unit_type`, `unit` و `default_value` رندر شود.
- **آمار کارت (ارتفاع، برگ، شاخه، میوه، محصول، نرخ)**: مستقیم از `plant` در پاسخ state.
- **سلامت گیاه**: با `is_healthy` و `can_continue` می‌توان پیام یا استایل متفاوت نشان داد (مثلاً هشدار وقتی `can_continue === false`).
- **نوارهای پیشرفت**: از `progress` برای پیشرفت رشد، وضعیت نور، وضعیت آب و محصول‌دهی.
- **چارت**: فقط `chart.labels` و آرایه‌های `height_history`, `leaf_history`, `yield_history`, `yield_rate_history` را به کامپوننت نمودار بدهید؛ با هر پاسخ جدید آرایه‌ها طولانی می‌شوند و نمودار به‌صورت تدریجی به‌روز می‌شود.
اگر endpointها یا فیلدهای اضافه‌ای در بکند دارید، می‌توان آنها را به همین سند اضافه کرد تا فرانت و بکند همیشه هم‌خوان باشند.
-223
View File
@@ -1,223 +0,0 @@
# مستندات APIهای توصیه و تشخیص
این سند سه گروه API را شرح می‌دهد: **توصیه آبیاری**، **تشخیص آفت** و **توصیه کوددهی**. همهٔ پاسخ‌ها در حال حاضر از دادهٔ ثابت (mock) برگردانده می‌شوند و پارامترهای ورودی در پاسخ استفاده نمی‌شوند.
**پایهٔ آدرس API:** `/api/`
**قالب کلی پاسخ:**
`{"status": "success", "data": <payload>}` — فقط با کد وضعیت HTTP 200.
---
## ۱. توصیه آبیاری (Irrigation Recommendation)
**پیشوند:** `api/irrigation-recommendation/`
### ۱.۱ دریافت تنظیمات (Config)
- **متد:** `GET`
- **آدرس:** `api/irrigation-recommendation/config/`
- **هدف:** برگرداندن اطلاعات مزرعه و لیست گزینه‌های محصول برای فرم توصیه آبیاری (هنگام بارگذاری صفحه).
- **ورودی:** ندارد. پارامترهای query خوانده یا استفاده نمی‌شوند.
**نمونه پاسخ:**
```json
{
"status": "success",
"data": {
"farmInfo": {
"soilType": "Loamy",
"waterQuality": "Medium EC",
"climateZone": "Temperate"
},
"cropOptions": [
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `farmInfo.soilType` | string | نوع خاک |
| `farmInfo.waterQuality` | string | کیفیت آب (مثلاً EC) |
| `farmInfo.climateZone` | string | منطقه اقلیمی |
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
---
### ۱.۲ دریافت توصیه آبیاری (Recommend)
- **متد:** `POST`
- **آدرس:** `api/irrigation-recommendation/recommend/`
- **هدف:** برگرداندن یک برنامهٔ آبیاری ثابت (تعداد در هفته، مدت، بهترین زمان، رطوبت، هشدار).
- **ورودی (بدن درخواست، اختیاری):** می‌توانید JSON با فیلدهایی مثل `crop_id`, `soilType`, `waterQuality`, `climateZone` بفرستید؛ در پاسخ فعلی استفاده نمی‌شوند.
- **CSRF:** این endpoint از CSRF معاف است (برای فراخوانی از فرانت بدون توکن).
**نمونه پاسخ:**
```json
{
"status": "success",
"data": {
"plan": {
"frequencyPerWeek": 4,
"durationMinutes": 45,
"bestTimeOfDay": "05:00 - 07:00",
"moistureLevel": 72,
"warning": "Avoid irrigation during midday hours in the coming week due to forecasted high temperatures."
}
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `plan.frequencyPerWeek` | number | تعداد آبیاری در هفته |
| `plan.durationMinutes` | number | مدت هر نوبت (دقیقه) |
| `plan.bestTimeOfDay` | string | بهترین بازه زمانی روز |
| `plan.moistureLevel` | number | سطح رطوبت هدف (درصد) |
| `plan.warning` | string | هشدار یا توصیه اضافه |
---
## ۲. تشخیص آفت (Pest Detection)
**پیشوند:** `api/pest-detection/`
### ۲.۱ تحلیل تصویر (Analyze)
- **متد:** `POST`
- **آدرس:** `api/pest-detection/analyze/`
- **هدف:** برگرداندن نتیجهٔ ثابت تشخیص آفت (نام آفت، اطمینان، توضیح، درمان) — برای زمانی که کاربر تصویر گیاه را آپلود و درخواست تحلیل می‌کند.
- **ورودی (بدن درخواست، اختیاری):** JSON یا form-data (مثلاً شامل `image` یا `file`). در پاسخ فعلی استفاده نمی‌شود.
- **CSRF:** این endpoint از CSRF معاف است.
**نمونه پاسخ:**
```json
{
"status": "success",
"data": {
"pest": "شپشک",
"confidence": 92,
"description": "حشرات کوچک مکنده شیره که باعث پیچ خوردگی برگ می‌شوند.",
"treatment": "یک بار در هفته از اسپری روغن نیم استفاده کنید."
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `pest` | string | نام آفت |
| `confidence` | number | درصد اطمینان (۰–۱۰۰) |
| `description` | string | توضیح کوتاه آفت |
| `treatment` | string | توصیه درمان |
---
## ۳. توصیه کوددهی (Fertilization Recommendation)
**پیشوند:** `api/fertilization-recommendation/`
### ۳.۱ دریافت تنظیمات (Config)
- **متد:** `GET`
- **آدرس:** `api/fertilization-recommendation/config/`
- **هدف:** برگرداندن دادهٔ مزرعه، مراحل رشد و گزینه‌های محصول برای فرم توصیه کوددهی (هنگام بارگذاری صفحه).
- **ورودی:** ندارد.
**نمونه پاسخ:**
```json
{
"status": "success",
"data": {
"farmData": {
"soilType": "Loamy",
"organicMatter": "Medium (2.5%)",
"waterEC": "1.2 dS/m"
},
"growthStages": [
{"id": "prePlanting", "icon": "tabler-seedling"},
{"id": "earlyGrowth", "icon": "tabler-leaf"},
{"id": "flowering", "icon": "tabler-flower"},
{"id": "fruiting", "icon": "tabler-apple"},
{"id": "postHarvest", "icon": "tabler-basket"}
],
"cropOptions": [
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `farmData.soilType` | string | نوع خاک |
| `farmData.organicMatter` | string | ماده آلی خاک |
| `farmData.waterEC` | string | EC آب |
| `growthStages` | array | مراحل رشد: `id`, `icon` |
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
---
### ۳.۲ دریافت توصیه کوددهی (Recommend)
- **متد:** `POST`
- **آدرس:** `api/fertilization-recommendation/recommend/`
- **هدف:** برگرداندن یک برنامهٔ کوددهی ثابت (نسبت NPK، مقدار در هکتار، روش و فاصله مصرف، استدلال).
- **ورودی (بدن درخواست، اختیاری):** JSON با فیلدهایی مثل `crop_id`, `growth_stage`, `soilType`, `organicMatter`, `waterEC`. در پاسخ فعلی استفاده نمی‌شوند.
- **CSRF:** این endpoint از CSRF معاف است.
**نمونه پاسخ:**
```json
{
"status": "success",
"data": {
"plan": {
"npkRatio": "20-20-20 (NPK)",
"amountPerHectare": "150 kg/ha",
"applicationMethod": "Foliar spray + soil broadcast",
"applicationInterval": "Every 14 days",
"reasoning": "Your loamy soil with medium organic matter (2.5%) provides good nutrient retention. Water EC of 1.2 dS/m indicates low salinity—suitable for most crops. At the flowering stage, increased phosphorus supports bloom development. We recommend a balanced NPK to maintain nitrogen for vegetative growth while boosting phosphorous for flowering."
}
}
}
```
| فیلد | نوع | توضیح |
|------|-----|--------|
| `plan.npkRatio` | string | نسبت NPK پیشنهادی |
| `plan.amountPerHectare` | string | مقدار مصرف در هکتار |
| `plan.applicationMethod` | string | روش مصرف (مثلاً محلول‌پاشی، خاکی) |
| `plan.applicationInterval` | string | فاصله بین مصرف |
| `plan.reasoning` | string | توضیح/استدلال توصیه |
---
## خلاصه Endpointها
| ماژول | متد | Endpoint |
|--------|------|----------|
| Irrigation | GET | `/api/irrigation-recommendation/config/` |
| Irrigation | POST | `/api/irrigation-recommendation/recommend/` |
| Pest Detection | POST | `/api/pest-detection/analyze/` |
| Fertilization | GET | `/api/fertilization-recommendation/config/` |
| Fertilization | POST | `/api/fertilization-recommendation/recommend/` |
---
**توجه:** در نسخهٔ فعلی هیچ پردازش، اعتبارسنجی یا استفاده از پارامترهای ورودی در پاسخ انجام نمی‌شود؛ همهٔ خروجی‌ها از دادهٔ ثابت (mock) هستند.
+2
View File
@@ -159,3 +159,5 @@ SIMPLE_JWT = {
"ROTATE_REFRESH_TOKENS": False, "ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False, "BLACKLIST_AFTER_ROTATION": False,
} }
CROP_ZONE_CHUNK_AREA_SQM = float(os.getenv("CROP_ZONE_CHUNK_AREA_SQM", "10000"))
+54
View File
@@ -0,0 +1,54 @@
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='CropArea',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
('geometry', models.JSONField(default=dict)),
('points', models.JSONField(default=list)),
('center', models.JSONField(default=dict)),
('area_sqm', models.FloatField()),
('area_hectares', models.FloatField()),
('chunk_area_sqm', models.FloatField()),
('zone_count', models.PositiveIntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'crop_areas',
'ordering': ['-created_at', '-id'],
},
),
migrations.CreateModel(
name='CropZone',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
('zone_id', models.CharField(max_length=64)),
('geometry', models.JSONField(default=dict)),
('points', models.JSONField(default=list)),
('center', models.JSONField(default=dict)),
('area_sqm', models.FloatField()),
('area_hectares', models.FloatField()),
('sequence', models.PositiveIntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('crop_area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='zones', to='crop_zoning.croparea')),
],
options={
'db_table': 'crop_zones',
'ordering': ['sequence', 'id'],
'constraints': [models.UniqueConstraint(fields=('crop_area', 'zone_id'), name='unique_crop_area_zone_id')],
},
),
]
View File
+51
View File
@@ -0,0 +1,51 @@
import uuid
from django.db import models
class CropArea(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
geometry = models.JSONField(default=dict)
points = models.JSONField(default=list)
center = models.JSONField(default=dict)
area_sqm = models.FloatField()
area_hectares = models.FloatField()
chunk_area_sqm = models.FloatField()
zone_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "crop_areas"
ordering = ["-created_at", "-id"]
def __str__(self):
return f"Area {self.uuid}"
class CropZone(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
crop_area = models.ForeignKey(
CropArea,
on_delete=models.CASCADE,
related_name="zones",
)
zone_id = models.CharField(max_length=64)
geometry = models.JSONField(default=dict)
points = models.JSONField(default=list)
center = models.JSONField(default=dict)
area_sqm = models.FloatField()
area_hectares = models.FloatField()
sequence = models.PositiveIntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "crop_zones"
ordering = ["sequence", "id"]
constraints = [
models.UniqueConstraint(fields=["crop_area", "zone_id"], name="unique_crop_area_zone_id"),
]
def __str__(self):
return self.zone_id
+202
View File
@@ -0,0 +1,202 @@
import math
from copy import deepcopy
from django.conf import settings
from django.db import transaction
from .models import CropArea, CropZone
EARTH_RADIUS_METERS = 6378137.0
def get_chunk_area_sqm():
raw_value = getattr(settings, "CROP_ZONE_CHUNK_AREA_SQM", 0)
try:
chunk_area = float(raw_value)
except (TypeError, ValueError):
chunk_area = 0
if chunk_area <= 0:
raise ValueError("CROP_ZONE_CHUNK_AREA_SQM must be a positive number.")
return chunk_area
def get_polygon_ring(area_feature):
geometry = (area_feature or {}).get("geometry", {})
coordinates = geometry.get("coordinates", [])
if not coordinates or not coordinates[0]:
raise ValueError("Area polygon coordinates are required.")
return coordinates[0]
def polygon_area_sqm(ring):
if len(ring) < 4:
return 0.0
latitudes = [point[1] for point in ring]
mean_latitude = math.radians(sum(latitudes) / len(latitudes))
projected_points = []
for longitude, latitude in ring:
x = math.radians(longitude) * EARTH_RADIUS_METERS * math.cos(mean_latitude)
y = math.radians(latitude) * EARTH_RADIUS_METERS
projected_points.append((x, y))
area = 0.0
for index in range(len(projected_points) - 1):
x1, y1 = projected_points[index]
x2, y2 = projected_points[index + 1]
area += (x1 * y2) - (x2 * y1)
return abs(area) / 2.0
def normalize_points(ring):
if len(ring) > 1 and ring[0] == ring[-1]:
ring = ring[:-1]
return [[point[0], point[1]] for point in ring]
def calculate_center(points):
if not points:
return {"longitude": 0.0, "latitude": 0.0}
longitude = sum(point[0] for point in points) / len(points)
latitude = sum(point[1] for point in points) / len(points)
return {
"longitude": round(longitude, 8),
"latitude": round(latitude, 8),
}
def build_zone_square(area_points, center, zone_area_sqm):
if len(area_points) < 4:
return area_points
width = math.sqrt(max(zone_area_sqm, 1))
half_width = width / 2.0
latitude_factor = 111320.0
longitude_factor = 111320.0 * math.cos(math.radians(center["latitude"]))
if longitude_factor == 0:
longitude_factor = 1.0
delta_lat = half_width / latitude_factor
delta_lng = half_width / longitude_factor
return [
[round(center["longitude"] - delta_lng, 8), round(center["latitude"] - delta_lat, 8)],
[round(center["longitude"] + delta_lng, 8), round(center["latitude"] - delta_lat, 8)],
[round(center["longitude"] + delta_lng, 8), round(center["latitude"] + delta_lat, 8)],
[round(center["longitude"] - delta_lng, 8), round(center["latitude"] + delta_lat, 8)],
]
def split_area_into_zones(area_feature):
area_ring = get_polygon_ring(area_feature)
area_points = normalize_points(area_ring)
area_center = calculate_center(area_points)
total_area_sqm = polygon_area_sqm(area_ring)
chunk_area_sqm = get_chunk_area_sqm()
zone_count = max(1, math.ceil(total_area_sqm / chunk_area_sqm))
zones = []
remaining_area = total_area_sqm
base_longitude = area_center["longitude"]
base_latitude = area_center["latitude"]
for sequence in range(zone_count):
zone_area_sqm = min(chunk_area_sqm, remaining_area) if sequence < zone_count - 1 else remaining_area
if zone_area_sqm <= 0:
zone_area_sqm = min(chunk_area_sqm, total_area_sqm)
shift = (sequence - ((zone_count - 1) / 2)) * 0.0003
zone_center = {
"longitude": round(base_longitude + shift, 8),
"latitude": round(base_latitude, 8),
}
zone_points = build_zone_square(area_points, zone_center, zone_area_sqm)
zone_geometry = {
"type": "Feature",
"properties": {
"zone_id": f"zone-{sequence}",
"sequence": sequence,
"area_sqm": round(zone_area_sqm, 2),
"area_hectares": round(zone_area_sqm / 10000, 4),
"center": zone_center,
},
"geometry": {
"type": "Polygon",
"coordinates": [[*zone_points, zone_points[0]]],
},
}
zones.append(
{
"zone_id": f"zone-{sequence}",
"geometry": zone_geometry,
"points": zone_points,
"center": zone_center,
"area_sqm": zone_area_sqm,
"area_hectares": zone_area_sqm / 10000,
"sequence": sequence,
}
)
remaining_area = max(0.0, remaining_area - zone_area_sqm)
area_geometry = deepcopy(area_feature)
area_geometry.setdefault("properties", {})
area_geometry["properties"].update(
{
"center": area_center,
"area_sqm": round(total_area_sqm, 2),
"area_hectares": round(total_area_sqm / 10000, 4),
}
)
return {
"area": {
"geometry": area_geometry,
"points": area_points,
"center": area_center,
"area_sqm": total_area_sqm,
"area_hectares": total_area_sqm / 10000,
"chunk_area_sqm": chunk_area_sqm,
"zone_count": zone_count,
},
"zones": zones,
}
def persist_zones(area_feature):
zoning_result = split_area_into_zones(area_feature)
area_data = zoning_result["area"]
with transaction.atomic():
crop_area = CropArea.objects.create(
geometry=area_data["geometry"],
points=area_data["points"],
center=area_data["center"],
area_sqm=round(area_data["area_sqm"], 2),
area_hectares=round(area_data["area_hectares"], 4),
chunk_area_sqm=round(area_data["chunk_area_sqm"], 2),
zone_count=area_data["zone_count"],
)
CropZone.objects.bulk_create(
[
CropZone(
crop_area=crop_area,
zone_id=zone["zone_id"],
geometry=zone["geometry"],
points=zone["points"],
center=zone["center"],
area_sqm=round(zone["area_sqm"], 2),
area_hectares=round(zone["area_hectares"], 4),
sequence=zone["sequence"],
)
for zone in zoning_result["zones"]
]
)
zoning_result["area"]["id"] = crop_area.id
zoning_result["area"]["uuid"] = str(crop_area.uuid)
return zoning_result
+51 -13
View File
@@ -5,6 +5,7 @@ Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
No processing, validation, or use of input parameters in responses. No processing, validation, or use of input parameters in responses.
""" """
from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, status from rest_framework import serializers, status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@@ -21,6 +22,7 @@ from .mock_data import (
ZONES_SOIL_QUALITY_RESPONSE_DATA, ZONES_SOIL_QUALITY_RESPONSE_DATA,
ZONES_WATER_NEED_RESPONSE_DATA, ZONES_WATER_NEED_RESPONSE_DATA,
) )
from .services import persist_zones
class AreaView(APIView): class AreaView(APIView):
@@ -88,23 +90,17 @@ class ZonesInitialView(APIView):
POST endpoint for initial zone data (map + hover/tooltip). POST endpoint for initial zone data (map + hover/tooltip).
Purpose: Purpose:
Accepts zones (FeatureCollection of grid squares) and returns static Accepts the main area polygon and creates zones based on configured
initial data per zone for map rendering and hover/tooltip display. area chunk size. Stores generated zones in database.
Does not include reason or criteria (those are in zone details).
Input parameters (body, JSON): Input parameters (body, JSON):
- zones: GeoJSON FeatureCollection. Location: body. Grid square polygons. - area: GeoJSON Feature. Location: body. Main land polygon.
- products: array of strings, optional. Location: body. Product IDs. If omitted, the static area from mock data is used.
Not read or used in response.
Response structure: Response structure:
- status: string, always "success". - status: string.
- data: object with total_area_hectares, total_area_sqm, zone_count, - data: object with total_area_hectares, total_area_sqm, zone_count,
zones (array of { zoneId, geometry, crop, matchPercent, waterNeed, chunk_area_sqm and zones.
estimatedProfit }).
No processing or validation is performed on inputs. Input values are
not used in the response.
""" """
@extend_schema( @extend_schema(
@@ -113,12 +109,54 @@ class ZonesInitialView(APIView):
responses={200: status_response("CropZoningZonesInitialResponse", data=serializers.JSONField())}, responses={200: status_response("CropZoningZonesInitialResponse", data=serializers.JSONField())},
) )
def post(self, request): def post(self, request):
area_feature = request.data.get("area") or AREA_RESPONSE_DATA.get("area")
try:
zoning_result = persist_zones(area_feature)
except ValueError as exc:
return Response(
{"status": "error", "message": str(exc)},
status=status.HTTP_400_BAD_REQUEST,
)
except ImproperlyConfigured as exc:
return Response(
{"status": "error", "message": str(exc)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
area_data = zoning_result["area"]
response_data = {
"area": {
"id": area_data["id"],
"uuid": area_data["uuid"],
"geometry": area_data["geometry"],
"points": area_data["points"],
"center": area_data["center"],
"area_sqm": round(area_data["area_sqm"], 2),
"area_hectares": round(area_data["area_hectares"], 4),
"chunk_area_sqm": round(area_data["chunk_area_sqm"], 2),
"zone_count": area_data["zone_count"],
},
"zones": [
{
"zoneId": zone["zone_id"],
"geometry": zone["geometry"],
"points": zone["points"],
"center": zone["center"],
"area_sqm": round(zone["area_sqm"], 2),
"area_hectares": round(zone["area_hectares"], 4),
}
for zone in zoning_result["zones"]
],
}
return Response( return Response(
{"status": "success", "data": ZONES_INITIAL_RESPONSE_DATA}, {"status": "success", "data": response_data},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
class ZonesWaterNeedView(APIView): class ZonesWaterNeedView(APIView):
""" """
POST endpoint for water need per zone (water need layer). POST endpoint for water need per zone (water need layer).
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "FAILURE", "status": "FAILURE",
"error": "Sample task failed." "error": "Sample task failed."
} }
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "PENDING", "status": "PENDING",
"message": "تسک در صف یا یافت نشد." "message": "تسک در صف یا یافت نشد."
} }
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "PROGRESS", "status": "PROGRESS",
"progress": { "progress": {
"current": 1, "current": 1,
@@ -2,8 +2,10 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "SUCCESS", "status": "SUCCESS",
"result": "done" "result": {
"$ref": "rag/chat-post_200_stream.json"
}
} }
} }
+63 -1
View File
@@ -25,8 +25,13 @@ class MockLoader:
selected_file = sorted(mock_files, key=self._mock_file_priority)[0] selected_file = sorted(mock_files, key=self._mock_file_priority)[0]
with selected_file.open("r", encoding="utf-8") as file: with selected_file.open("r", encoding="utf-8") as file:
service_root = self.base_path / service_name
return LoadedMockResponse( return LoadedMockResponse(
data=json.load(file), data=self._resolve_references(
json.load(file),
current_file=selected_file,
service_root=service_root,
),
status_code=self._extract_status_code(selected_file), status_code=self._extract_status_code(selected_file),
file_path=str(selected_file), file_path=str(selected_file),
) )
@@ -99,3 +104,60 @@ class MockLoader:
elif "pending" in stem or "progress" in stem: elif "pending" in stem or "progress" in stem:
keyword_rank = 1 keyword_rank = 1
return (status_code >= 400, keyword_rank, stem) return (status_code >= 400, keyword_rank, stem)
def _resolve_references(self, value, current_file, service_root, seen=None):
current_file = current_file.resolve()
service_root = service_root.resolve()
seen = seen or {current_file}
if isinstance(value, list):
return [
self._resolve_references(item, current_file=current_file, service_root=service_root, seen=seen)
for item in value
]
if not isinstance(value, dict):
return value
ref_path = value.get("$ref")
if isinstance(ref_path, str) and len(value) == 1:
referenced_file = self._resolve_reference_path(
ref_path=ref_path,
current_file=current_file,
service_root=service_root,
)
if referenced_file in seen:
raise MockFileNotFound(f"Circular mock reference detected for '{referenced_file}'.")
with referenced_file.open("r", encoding="utf-8") as file:
return self._resolve_references(
json.load(file),
current_file=referenced_file,
service_root=service_root,
seen=seen | {referenced_file},
)
return {
key: self._resolve_references(item, current_file=current_file, service_root=service_root, seen=seen)
for key, item in value.items()
}
@staticmethod
def _resolve_reference_path(ref_path, current_file, service_root):
candidates = [
current_file.parent / ref_path,
service_root / ref_path,
]
service_root_resolved = service_root.resolve()
for candidate in candidates:
candidate_resolved = candidate.resolve()
try:
candidate_resolved.relative_to(service_root_resolved)
except ValueError:
continue
if candidate_resolved.is_file():
return candidate_resolved
raise MockFileNotFound(f"Referenced mock file '{ref_path}' was not found.")
+58
View File
@@ -0,0 +1,58 @@
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from rest_framework.test import APIRequestFactory, force_authenticate
from .models import Conversation, Message
from .views import ChatTaskStatusView
@override_settings(USE_EXTERNAL_API_MOCK=True)
class ChatTaskStatusViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="farmer",
password="secret123",
email="farmer@example.com",
phone_number="09120000000",
)
self.conversation = Conversation.objects.create(
owner=self.user,
title="Irrigation chat",
farm_context={},
)
self.user_message = Message.objects.create(
conversation=self.conversation,
role=Message.ROLE_USER,
content="What is the best irrigation plan?",
raw_response={
"task_id": "farm-ai-chat-task-123",
"status": "PENDING",
"status_url": "/api/tasks/farm-ai-chat-task-123/status/",
},
)
def test_status_success_uses_chat_mock_result_and_persists_assistant_message(self):
request = self.factory.get("/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/")
force_authenticate(request, user=self.user)
response = ChatTaskStatusView.as_view()(request, task_id="farm-ai-chat-task-123")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["status"], "success")
self.assertEqual(response.data["data"]["task_id"], "farm-ai-chat-task-123")
self.assertEqual(response.data["data"]["status"], "SUCCESS")
self.assertEqual(response.data["data"]["conversation_id"], str(self.conversation.uuid))
self.assertEqual(response.data["data"]["result"]["content"], "Here is the recommended plan.")
self.assertEqual(len(response.data["data"]["result"]["sections"]), 3)
self.assertEqual(response.data["data"]["result"]["task_id"], "farm-ai-chat-task-123")
assistant_message = (
self.conversation.messages.filter(role=Message.ROLE_ASSISTANT)
.order_by("-created_at")
.first()
)
self.assertIsNotNone(assistant_message)
self.assertEqual(assistant_message.content, "Here is the recommended plan.")
self.assertEqual(assistant_message.raw_response["task_id"], "farm-ai-chat-task-123")
self.assertEqual(len(assistant_message.raw_response["sections"]), 3)
+1 -1
View File
@@ -12,7 +12,7 @@ from .views import (
urlpatterns = [ urlpatterns = [
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"), path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"), # path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
path("chat/task/", ChatTaskCreateView.as_view(), name="farm-ai-assistant-chat-task-create"), path("chat/task/", ChatTaskCreateView.as_view(), name="farm-ai-assistant-chat-task-create"),
path("chat/task/<str:task_id>/status/", ChatTaskStatusView.as_view(), name="farm-ai-assistant-chat-task-status"), path("chat/task/<str:task_id>/status/", ChatTaskStatusView.as_view(), name="farm-ai-assistant-chat-task-status"),
path("chats/", ChatListCreateView.as_view(), name="farm-ai-assistant-chat-list-create"), path("chats/", ChatListCreateView.as_view(), name="farm-ai-assistant-chat-list-create"),
+27 -1
View File
@@ -201,6 +201,29 @@ class ConversationAccessMixin:
return task_status_payload return task_status_payload
def _extract_structured_task_result(self, adapter_data):
payload_source = adapter_data
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
payload_source = adapter_data["data"]
if not isinstance(payload_source, dict):
return None
result = payload_source.get("result")
if isinstance(result, dict):
return result
if payload_source.get("status") == "SUCCESS":
content = payload_source.get("content")
sections = payload_source.get("sections")
if content or sections:
return {
"content": content or "",
"sections": sections or [],
}
return None
@staticmethod @staticmethod
def _serialize_chat_message(message): def _serialize_chat_message(message):
raw_response = message.raw_response if isinstance(message.raw_response, dict) else {} raw_response = message.raw_response if isinstance(message.raw_response, dict) else {}
@@ -541,7 +564,10 @@ class ChatTaskStatusView(ConversationAccessMixin, APIView):
conversation_id=conversation_id, conversation_id=conversation_id,
) )
result = task_status_payload.get("result") result = self._extract_structured_task_result(adapter_response.data)
if result is not None:
task_status_payload["result"] = result
if user_message and task_status_payload.get("status") == "SUCCESS" and isinstance(result, dict): if user_message and task_status_payload.get("status") == "SUCCESS" and isinstance(result, dict):
assistant_payload = self._persist_task_result(user_message, task_id, result) assistant_payload = self._persist_task_result(user_message, task_id, result)
task_status_payload["result"] = assistant_payload task_status_payload["result"] = assistant_payload
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "FAILURE", "status": "FAILURE",
"error": "Sample task failed." "error": "Sample task failed."
} }
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "PENDING", "status": "PENDING",
"message": "تسک در صف یا یافت نشد." "message": "تسک در صف یا یافت نشد."
} }
@@ -2,7 +2,7 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "PROGRESS", "status": "PROGRESS",
"progress": { "progress": {
"current": 1, "current": 1,
@@ -2,8 +2,10 @@
"code": 200, "code": 200,
"msg": "success", "msg": "success",
"data": { "data": {
"task_id": "sample-task-123", "task_id": "farm-ai-chat-task-123",
"status": "SUCCESS", "status": "SUCCESS",
"result": "done" "result": {
"$ref": "rag/chat-post_200_stream.json"
}
} }
} }