Refactor CropZoningMap and related components for improved API integration and UI enhancements

- Updated CropZoningMap to utilize new ZoneInitialData type for zone click handling and added zonesData prop for API-driven zone rendering.
- Removed deprecated crop zoning mock data file and integrated grid creation logic for initial zone fetching.
- Enhanced CropZoningWrapper to manage area and zone data loading states, improving user experience with asynchronous data fetching.
- Updated ZoneDetailPanel to handle loading states and display product labels dynamically based on fetched data.
- Refactored ZoneLegend to conditionally render items based on available product data, enhancing visual feedback during loading.
This commit is contained in:
2026-02-26 00:17:32 +03:30
parent aad5b1c2bd
commit 3db9a86cbf
9 changed files with 981 additions and 158 deletions
@@ -0,0 +1,384 @@
# مستندات APIهای زون‌بندی کشت (Crop Zoning)
این سند تمام APIهای مورد نیاز برای صفحه **Crop Zoning** را شرح می‌دهد: ورودی‌ها، خروجی‌ها، محصولات، رنگ‌ها، مساحت کلی و دیتای هر بخش زمین به صورت جداگانه.
**مسیر صفحه:** `(dashboard)/(private)/crop-zoning`
**کامپوننت اصلی:** `CropZoningWrapper`
---
## نمای کلی و جریان درخواست‌ها
```
۱. GET area → منطقهٔ ثابت (کاربر امکان رسم ندارد)
۲. GET products → لیست محصولات و رنگ‌ها
۳. POST zones/initial → ارسال محدودهٔ مربع‌ها → دریافت دیتای اولیه (نقشه + هاور/tooltip)
۴. 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 | بله | محصول پیشنهادی (رنگ نقشه + tooltip) |
| `matchPercent` | number | بله | درصد تطابق (هاور/tooltip) |
| `waterNeed` | string | بله | نیاز آبی (هاور/tooltip) |
| `estimatedProfit` | string | بله | سود تخمینی (هاور/tooltip) |
**نکته:** این فیلدها هنگام **هاور** روی مربع در tooltip نمایش داده می‌شوند؛ نیازی به درخواست جداگانه برای tooltip نیست.
---
## ۳. 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. **کلیک روی مربع:** `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 نمایش داده شود.
@@ -0,0 +1,206 @@
{
"_comment": "دیتای ماک مطابق CROP_ZONING_APIS.md",
"_source": "CROP_ZONING_APIS.md",
"area": {
"description": "GET /api/crop-zoning/area/ — منطقهٔ اولیه (کاربر نمی‌تواند رسم کند)",
"response": {
"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]]
]
}
}
}
}
},
"products": {
"description": "GET /api/crop-zoning/products/",
"response": {
"status": "success",
"data": {
"products": [
{ "id": "wheat", "label": "گندم", "color": "#6bcb77" },
{ "id": "canola", "label": "کلزا", "color": "#ffd93d" },
{ "id": "saffron", "label": "زعفران", "color": "#9b59b6" }
]
}
}
},
"zonesInitialRequest": {
"description": "POST /api/crop-zoning/zones/initial/ — Request Body",
"body": {
"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 }
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[51.38, 35.6815], [51.3815, 35.6815], [51.3815, 35.683], [51.38, 35.683], [51.38, 35.6815]]
]
},
"properties": { "index": 2 }
}
]
},
"products": ["wheat", "canola", "saffron"]
}
},
"zonesInitialResponse": {
"description": "POST /api/crop-zoning/zones/initial/ — Response",
"response": {
"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": [
[[51.3815, 35.68], [51.383, 35.68], [51.383, 35.6815], [51.3815, 35.6815], [51.3815, 35.68]]
]
},
"crop": "canola",
"matchPercent": 78,
"waterNeed": "۵۰۰۰-۶۰۰۰ m³/ha",
"estimatedProfit": "۲۰-۳۵ میلیون/هکتار"
},
{
"zoneId": "zone-2",
"geometry": {
"type": "Polygon",
"coordinates": [
[[51.38, 35.6815], [51.3815, 35.6815], [51.3815, 35.683], [51.38, 35.683], [51.38, 35.6815]]
]
},
"crop": "saffron",
"matchPercent": 92,
"waterNeed": "۳۰۰۰-۴۰۰۰ m³/ha",
"estimatedProfit": "۵۰-۱۵۰ میلیون/هکتار"
}
]
}
}
},
"zoneDetails": {
"description": "GET /api/crop-zoning/zones/:zoneId/details/ — Response per zone",
"responses": {
"zone-0": {
"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
}
},
"zone-1": {
"status": "success",
"data": {
"zoneId": "zone-1",
"crop": "canola",
"matchPercent": 78,
"waterNeed": "۵۰۰۰-۶۰۰۰ m³/ha",
"estimatedProfit": "۲۰-۳۵ میلیون/هکتار",
"reason": "شرایط اقلیمی مساعد، نیاز آبی قابل تأمین",
"criteria": [
{ "name": "دما", "value": 75 },
{ "name": "بارش", "value": 72 },
{ "name": "خاک", "value": 80 },
{ "name": "آب", "value": 78 }
],
"area_hectares": 2.25
}
},
"zone-2": {
"status": "success",
"data": {
"zoneId": "zone-2",
"crop": "saffron",
"matchPercent": 92,
"waterNeed": "۳۰۰۰-۴۰۰۰ m³/ha",
"estimatedProfit": "۵۰-۱۵۰ میلیون/هکتار",
"reason": "ارتفاع و آب و هوای خشک مناسب، پتانسیل سود بالا",
"criteria": [
{ "name": "دما", "value": 90 },
{ "name": "بارش", "value": 65 },
{ "name": "خاک", "value": 95 },
{ "name": "آب", "value": 85 }
],
"area_hectares": 2.25
}
}
}
},
"areaGeoJson": {
"description": "منطقهٔ ثابت (MOCK_AREA_GEOJSON) — polygon اصلی نقشه",
"feature": {
"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]]
]
}
}
}
}