This commit is contained in:
2026-04-11 03:50:23 +03:30
parent 76362309c8
commit d43bd74a06
30 changed files with 1024 additions and 758 deletions
-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
```
+12 -1
View File
@@ -142,7 +142,18 @@
"menuLevels": "سطح‌های منو",
"menuLevel2": "سطح منو 2",
"menuLevel3": "سطح منو 3",
"disabledMenu": "منوی غیرفعال"
"disabledMenu": "منوی غیرفعال",
"farmDomain": "مدیریت مزرعه",
"farmDashboard": "داشبورد مزرعه",
"cropHealth": "سلامت محصول",
"waterWeather": "آب و هوا",
"soilAnalytics": "تحلیل خاک",
"yieldHarvest": "عملکرد و برداشت",
"farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی",
"sensorSection": "سنسورها",
"sensor7In1": "سنسور خاک 7 در 1"
},
"farmDashboard": {
"settings": {
@@ -0,0 +1,7 @@
import CropHealthPageWrapper from '@views/dashboards/farm/CropHealthPageWrapper'
const CropHealthPage = async () => {
return <CropHealthPageWrapper />
}
export default CropHealthPage
@@ -0,0 +1,7 @@
import EconomicOverviewPageWrapper from '@views/dashboards/farm/EconomicOverviewPageWrapper'
const EconomicOverviewPage = async () => {
return <EconomicOverviewPageWrapper />
}
export default EconomicOverviewPage
@@ -0,0 +1,7 @@
import AlertsPageWrapper from '@views/dashboards/farm/AlertsPageWrapper'
const FarmAlertsPage = async () => {
return <AlertsPageWrapper />
}
export default FarmAlertsPage
@@ -0,0 +1,7 @@
import PestRiskPageWrapper from '@views/dashboards/farm/PestRiskPageWrapper'
const PestRiskPage = async () => {
return <PestRiskPageWrapper />
}
export default PestRiskPage
@@ -0,0 +1,7 @@
import YieldHarvestPageWrapper from '@views/dashboards/farm/YieldHarvestPageWrapper'
const YieldHarvestPage = async () => {
return <YieldHarvestPageWrapper />
}
export default YieldHarvestPage
@@ -94,6 +94,23 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
>
{t('dashboards')}
</MenuItem>
<MenuSection label={t('farmDomain')}>
<MenuItem href="/crop-health" icon={<i className="tabler-plant" />}>
{t('cropHealth')}
</MenuItem>
<MenuItem href="/yield-harvest" icon={<i className="tabler-chart-line" />}>
{t('yieldHarvest')}
</MenuItem>
<MenuItem href="/farm-alerts" icon={<i className="tabler-alert-triangle" />}>
{t('farmAlerts')}
</MenuItem>
<MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}>
{t('pestDiseaseRisk')}
</MenuItem>
<MenuItem href="/economic-overview" icon={<i className="tabler-currency-dollar" />}>
{t('economicOverview')}
</MenuItem>
</MenuSection>
<MenuSection label={t('dataSection')}>
<MenuItem href="/water-data" icon={<i className="tabler-droplet" />}>
{t('waterData')}
@@ -105,9 +122,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
{t('cropZoning')}
</MenuItem>
</MenuSection>
<MenuSection label={t('sesnorSection')}>
<MenuSection label={t('sensorSection')}>
<MenuItem href="/solid-sensor" icon={<i className="tabler-sensor" />}>
Sensor 7
{t('sensor7In1')}
</MenuItem>
</MenuSection>
+2
View File
@@ -4,6 +4,8 @@ export const navigationLabels = {
farm: 'داشبورد مزرعه',
waterData: 'دیتاهای آب',
soilData: 'اطلاعات خاک',
sensorSection: 'سنسورها',
sensor7In1: 'سنسور خاک 7 در 1',
dataSection: 'بخش داده‌ها',
crm: 'مدیریت ارتباط با مشتری',
analytics: 'تحلیل‌ها',
+12 -1
View File
@@ -100,6 +100,17 @@
"menuLevels": "مستويات القائمة",
"menuLevel2": "مستوى القائمة 2",
"menuLevel3": "مستوى القائمة 3",
"disabledMenu": "قائمة المعوقين"
"disabledMenu": "قائمة المعوقين",
"farmDomain": "إدارة المزرعة",
"farmDashboard": "لوحة المزرعة",
"cropHealth": "صحة المحصول",
"waterWeather": "المياه والطقس",
"soilAnalytics": "تحليل التربة",
"yieldHarvest": "الإنتاج والحصاد",
"farmAlerts": "تنبيهات المزرعة",
"pestDiseaseRisk": "مخاطر الآفات والأمراض",
"economicOverview": "النظرة الاقتصادية",
"sensorSection": "المستشعرات",
"sensor7In1": "مستشعر التربة 7 في 1"
}
}
+12 -1
View File
@@ -100,6 +100,17 @@
"menuLevels": "Menu Levels",
"menuLevel2": "Menu Level 2",
"menuLevel3": "Menu Level 3",
"disabledMenu": "Disabled Menu"
"disabledMenu": "Disabled Menu",
"farmDomain": "Farm Management",
"farmDashboard": "Farm Dashboard",
"cropHealth": "Crop Health",
"waterWeather": "Water & Weather",
"soilAnalytics": "Soil Analytics",
"yieldHarvest": "Yield & Harvest",
"farmAlerts": "Farm Alerts",
"pestDiseaseRisk": "Pest & Disease Risk",
"economicOverview": "Economic Overview",
"sensorSection": "Sensors",
"sensor7In1": "Soil Sensor 7-in-1"
}
}
+12 -3
View File
@@ -100,8 +100,17 @@
"menuLevels": "سطح‌های منو",
"menuLevel2": "سطح منو 2",
"menuLevel3": "سطح منو 3",
"disabledMenu": "منوی غیرفعال"
"disabledMenu": "منوی غیرفعال",
"farmDomain": "مدیریت مزرعه",
"farmDashboard": "داشبورد مزرعه",
"cropHealth": "سلامت محصول",
"waterWeather": "آب و هوا",
"soilAnalytics": "تحلیل خاک",
"yieldHarvest": "عملکرد و برداشت",
"farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی",
"sensorSection": "سنسورها",
"sensor7In1": "سنسور خاک 7 در 1"
}
}
+12 -1
View File
@@ -100,6 +100,17 @@
"menuLevels": "Niveaux de menus",
"menuLevel2": "Niveau menu 2",
"menuLevel3": "Niveau menu 3",
"disabledMenu": "Menu désactivé"
"disabledMenu": "Menu désactivé",
"farmDomain": "Gestion de la ferme",
"farmDashboard": "Tableau de bord ferme",
"cropHealth": "Santé des cultures",
"waterWeather": "Eau et météo",
"soilAnalytics": "Analyse du sol",
"yieldHarvest": "Rendement et récolte",
"farmAlerts": "Alertes ferme",
"pestDiseaseRisk": "Risque ravageurs et maladies",
"economicOverview": "Aperçu économique",
"sensorSection": "Capteurs",
"sensor7In1": "Capteur de sol 7-en-1"
}
}
+122
View File
@@ -36,6 +36,128 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
}
]
},
{
label: 'farmDomain',
icon: 'tabler-plant-2',
children: [
{
label: 'farmDashboard',
icon: 'tabler-dashboard',
href: '/dashboard'
},
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{
label: 'waterWeather',
icon: 'tabler-droplet',
href: '/water-data'
},
{
label: 'soilAnalytics',
icon: 'tabler-grain',
href: '/soil-data'
},
{
label: 'yieldHarvest',
icon: 'tabler-chart-line',
href: '/yield-harvest'
},
{
label: 'plantSimulator',
icon: 'tabler-seeding',
href: '/plant-simulator'
},
{
label: 'farmAlerts',
icon: 'tabler-alert-triangle',
href: '/farm-alerts'
},
{
label: 'pestDiseaseRisk',
icon: 'tabler-bug',
href: '/pest-risk'
},
{
label: 'economicOverview',
icon: 'tabler-currency-dollar',
href: '/economic-overview'
},
{
label: 'cropZoning',
icon: 'tabler-map-pin',
href: '/crop-zoning'
},
{
label: 'farmAiAssistant',
icon: 'tabler-robot',
href: '/farm-ai-assistant'
}
]
},
{
label: 'farmDomain',
icon: 'tabler-plant-2',
children: [
{
label: 'farmDashboard',
icon: 'tabler-dashboard',
href: '/dashboard'
},
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{
label: 'waterWeather',
icon: 'tabler-droplet',
href: '/water-data'
},
{
label: 'soilAnalytics',
icon: 'tabler-grain',
href: '/soil-data'
},
{
label: 'yieldHarvest',
icon: 'tabler-chart-line',
href: '/yield-harvest'
},
{
label: 'plantSimulator',
icon: 'tabler-seeding',
href: '/plant-simulator'
},
{
label: 'farmAlerts',
icon: 'tabler-alert-triangle',
href: '/farm-alerts'
},
{
label: 'pestDiseaseRisk',
icon: 'tabler-bug',
href: '/pest-risk'
},
{
label: 'economicOverview',
icon: 'tabler-currency-dollar',
href: '/economic-overview'
},
{
label: 'cropZoning',
icon: 'tabler-map-pin',
href: '/crop-zoning'
},
{
label: 'farmAiAssistant',
icon: 'tabler-robot',
href: '/farm-ai-assistant'
}
]
},
{
label: 'apps',
icon: 'tabler-mail',
+61
View File
@@ -40,6 +40,67 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
}
]
},
{
label: 'farmDomain',
isSection: true,
children: [
{
label: 'farmDashboard',
icon: 'tabler-dashboard',
href: '/dashboard'
},
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{
label: 'waterWeather',
icon: 'tabler-droplet',
href: '/water-data'
},
{
label: 'soilAnalytics',
icon: 'tabler-grain',
href: '/soil-data'
},
{
label: 'yieldHarvest',
icon: 'tabler-chart-line',
href: '/yield-harvest'
},
{
label: 'plantSimulator',
icon: 'tabler-seeding',
href: '/plant-simulator'
},
{
label: 'farmAlerts',
icon: 'tabler-alert-triangle',
href: '/farm-alerts'
},
{
label: 'pestDiseaseRisk',
icon: 'tabler-bug',
href: '/pest-risk'
},
{
label: 'economicOverview',
icon: 'tabler-currency-dollar',
href: '/economic-overview'
},
{
label: 'cropZoning',
icon: 'tabler-map-pin',
href: '/crop-zoning'
},
{
label: 'farmAiAssistant',
icon: 'tabler-robot',
href: '/farm-ai-assistant'
}
]
},
{
label: 'frontPages',
icon: 'tabler-files',
+25 -1
View File
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
export const FARM_HUB_STORAGE_KEY = "farm_hub";
const LEGACY_SENSOR_HUB_STORAGE_KEY = "sensor_hub";
const FARM_HUB_UPDATED_EVENT = "farm-hub-updated";
export interface FarmHubInfo {
farm_uuid: string;
@@ -70,7 +71,28 @@ export const useFarmHub = (): UseFarmHubReturn => {
const [farmHub, setFarmHubState] = useState<FarmHubInfo | null>(null);
useEffect(() => {
setFarmHubState(getStoredFarmHub());
const syncFarmHub = () => {
setFarmHubState(getStoredFarmHub());
};
const handleStorage = (event: StorageEvent) => {
if (
event.key === null ||
event.key === FARM_HUB_STORAGE_KEY ||
event.key === LEGACY_SENSOR_HUB_STORAGE_KEY
) {
syncFarmHub();
}
};
syncFarmHub();
window.addEventListener("storage", handleStorage);
window.addEventListener(FARM_HUB_UPDATED_EVENT, syncFarmHub);
return () => {
window.removeEventListener("storage", handleStorage);
window.removeEventListener(FARM_HUB_UPDATED_EVENT, syncFarmHub);
};
}, []);
const setFarmHub = useCallback((data: FarmHubInfo | null) => {
@@ -79,6 +101,7 @@ export const useFarmHub = (): UseFarmHubReturn => {
if (data === null) {
localStorage.removeItem(FARM_HUB_STORAGE_KEY);
setFarmHubState(null);
window.dispatchEvent(new Event(FARM_HUB_UPDATED_EVENT));
return;
}
@@ -90,6 +113,7 @@ export const useFarmHub = (): UseFarmHubReturn => {
localStorage.setItem(FARM_HUB_STORAGE_KEY, JSON.stringify(normalized));
setFarmHubState(normalized);
window.dispatchEvent(new Event(FARM_HUB_UPDATED_EVENT));
}, []);
const getFarmUuid = useCallback(() => {
@@ -0,0 +1,27 @@
import { apiClient } from '../client'
const PREFIX = '/api/crop-health'
export interface CropHealthSummary {
farm_health_score?: Record<string, unknown>
ndviHealthCard?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
}
function extract<T>(res: ApiResponse<T> | T): T {
return res && typeof res === 'object' && 'data' in res ? (res as ApiResponse<T>).data : (res as T)
}
export const cropHealthService = {
async getSummary(farmUuid: string): Promise<CropHealthSummary> {
const res = await apiClient.get<ApiResponse<CropHealthSummary> | CropHealthSummary>(
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
}
@@ -0,0 +1,34 @@
import { apiClient } from '../client'
const PREFIX = '/api/economic-overview'
export interface EconomicOverviewSummary {
economicOverview?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
result?: T
}
function extract<T>(res: ApiResponse<T> | T): T {
if (res && typeof res === 'object') {
if ('data' in res) return (res as ApiResponse<T>).data
if ('result' in res) return (res as ApiResponse<T>).result as T
}
return res as T
}
export const economicOverviewService = {
async getSummary(farmUuid: string): Promise<EconomicOverviewSummary> {
const res = await apiClient.get<ApiResponse<EconomicOverviewSummary> | EconomicOverviewSummary>(
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
const data = extract(res) as Record<string, unknown>
return 'economicOverview' in data ? (data as EconomicOverviewSummary) : { economicOverview: data }
},
}
@@ -0,0 +1,48 @@
import { apiClient } from '../client'
const PREFIX = '/api/farm-alerts'
export interface FarmAlertsSummary {
tracker?: Record<string, unknown>
timeline?: Record<string, unknown>
recommendations?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
result?: T
}
function extract<T>(res: ApiResponse<T> | T): T {
if (res && typeof res === 'object') {
if ('data' in res) return (res as ApiResponse<T>).data
if ('result' in res) return (res as ApiResponse<T>).result as T
}
return res as T
}
export const farmAlertsService = {
async getTracker(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/tracker/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getTimeline(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/timeline/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getRecommendations(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/recommendations/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
}
@@ -0,0 +1,44 @@
import { apiClient } from '../client'
const PREFIX = '/api/pest-detection'
export interface PestRiskSummary {
disease_risk?: Record<string, unknown>
pest_risk?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
result?: T
}
function extract<T>(res: ApiResponse<T> | T): T {
if (res && typeof res === 'object') {
if ('data' in res) return (res as ApiResponse<T>).data
if ('result' in res) return (res as ApiResponse<T>).result as T
}
return res as T
}
function toKpiCard(card?: Record<string, unknown>): Record<string, unknown> {
if (!card || typeof card !== 'object') return {}
return { kpis: [card] }
}
export const pestDetectionDomainService = {
async getRiskSummary(farmUuid: string): Promise<PestRiskSummary> {
const res = await apiClient.get<ApiResponse<PestRiskSummary> | PestRiskSummary>(
`${PREFIX}/risk-summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
const data = extract(res)
return {
disease_risk: toKpiCard(data?.disease_risk),
pest_risk: toKpiCard(data?.pest_risk)
}
},
}
+65
View File
@@ -0,0 +1,65 @@
import { apiClient } from '../client'
const PREFIX = '/api/soil'
export interface SoilSummary {
avg_soil_moisture?: Record<string, unknown>
sensorRadarChart?: Record<string, unknown>
sensorComparisonChart?: Record<string, unknown>
anomalyDetectionCard?: Record<string, unknown>
soilMoistureHeatmap?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
}
function extract<T>(res: ApiResponse<T> | T): T {
return res && typeof res === 'object' && 'data' in res ? (res as ApiResponse<T>).data : (res as T)
}
export const soilService = {
async getSummary(farmUuid: string): Promise<SoilSummary> {
const res = await apiClient.get<ApiResponse<SoilSummary> | SoilSummary>(
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getAvgMoisture(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/avg-moisture/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getSensorRadarChart(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/sensor-radar-chart/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getSensorComparisonChart(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/sensor-comparison-chart/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getAnomalies(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/anomalies/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getMoistureHeatmap(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/moisture-heatmap/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
}
+49
View File
@@ -0,0 +1,49 @@
import { apiClient } from '../client'
const PREFIX = '/api/water'
export interface WaterSummary {
farmWeatherCard?: Record<string, unknown>
waterNeedPrediction?: Record<string, unknown>
water_stress_index?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
}
function extract<T>(res: ApiResponse<T> | T): T {
return res && typeof res === 'object' && 'data' in res ? (res as ApiResponse<T>).data : (res as T)
}
export const waterService = {
async getSummary(farmUuid: string): Promise<WaterSummary> {
const res = await apiClient.get<ApiResponse<WaterSummary> | WaterSummary>(
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getCard(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/card/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getNeedPrediction(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/need-prediction/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
async getStressIndex(farmUuid: string): Promise<Record<string, unknown>> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/stress-index/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
return extract(res)
},
}
@@ -0,0 +1,46 @@
import { apiClient } from '../client'
const PREFIX = '/api/yield-harvest'
export interface YieldHarvestSummary {
yield_prediction?: Record<string, unknown>
yieldPredictionChart?: Record<string, unknown>
harvestPredictionCard?: Record<string, unknown>
}
interface ApiResponse<T> {
code: number
msg: string
data: T
result?: T
}
function extract<T>(res: ApiResponse<T> | T): T {
if (res && typeof res === 'object') {
if ('data' in res) return (res as ApiResponse<T>).data
if ('result' in res) return (res as ApiResponse<T>).result as T
}
return res as T
}
function toKpiCard(card?: Record<string, unknown>): Record<string, unknown> {
if (!card || typeof card !== 'object') return {}
return { kpis: [card] }
}
export const yieldHarvestService = {
async getSummary(farmUuid: string): Promise<YieldHarvestSummary> {
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
)
const data = extract(res)
return {
yield_prediction: toKpiCard(data?.yield_prediction ?? data?.yield_prediction_card),
yieldPredictionChart: (data?.yieldPredictionChart ?? data?.yield_prediction_chart ?? {}) as Record<string, unknown>,
harvestPredictionCard: (data?.harvestPredictionCard ?? data?.harvest_prediction_card ?? {}) as Record<string, unknown>
}
},
}
@@ -0,0 +1,81 @@
'use client'
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker'
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
import RecommendationsList from '@views/dashboards/farm/RecommendationsList'
import { farmAlertsService } from '@/libs/api/services/farmAlertsService'
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
const AlertsPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [tracker, setTracker] = useState<Record<string, unknown>>({})
const [timeline, setTimeline] = useState<Record<string, unknown>>({})
const [recommendations, setRecommendations] = useState<Record<string, unknown>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setTracker({})
setTimeline({})
setRecommendations({})
setLoading(false)
return
}
setLoading(true)
Promise.all([
farmAlertsService.getTracker(farmUuid).catch(() => ({})),
farmAlertsService.getTimeline(farmUuid).catch(() => ({})),
farmAlertsService.getRecommendations(farmUuid).catch(() => ({}))
])
.then(([t, tl, r]) => {
setTracker(t)
setTimeline(tl)
setRecommendations(r)
})
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} sx={cardRowSx}>
<FarmAlertsTracker data={tracker} />
</Grid>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'stretch' }}>
<Grid size={{ xs: 12, lg: 4 }} sx={cardRowSx}>
<FarmAlertsTimeline data={timeline} />
</Grid>
<Grid size={{ xs: 12, lg: 8 }} sx={cardRowSx}>
<RecommendationsList data={recommendations} />
</Grid>
</Grid>
</Grid>
</Box>
)
}
export default AlertsPageWrapper
@@ -0,0 +1,65 @@
'use client'
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs'
import NDVIHealthCard from '@views/dashboards/farm/NDVIHealthCard'
import { cropHealthService } from '@/libs/api/services/cropHealthService'
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
const CropHealthPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [data, setData] = useState<Record<string, unknown>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
cropHealthService
.getSummary(farmUuid)
.then(summary => setData((summary as Record<string, unknown>) ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.farm_health_score as Record<string, unknown>} />
</Grid>
<Grid size={12} sx={cardRowSx}>
<NDVIHealthCard data={data.ndviHealthCard as Record<string, unknown>} />
</Grid>
</Grid>
</Box>
)
}
export default CropHealthPageWrapper
@@ -0,0 +1,62 @@
'use client'
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import EconomicOverview from '@views/dashboards/farm/EconomicOverview'
import { economicOverviewService } from '@/libs/api/services/economicOverviewService'
import type { EconomicOverviewSummary } from '@/libs/api/services/economicOverviewService'
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
const EconomicOverviewPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [data, setData] = useState<EconomicOverviewSummary>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
economicOverviewService
.getSummary(farmUuid)
.then(summary => setData(summary ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} sx={cardRowSx}>
<EconomicOverview data={data.economicOverview as Record<string, unknown>} />
</Grid>
</Grid>
</Box>
)
}
export default EconomicOverviewPageWrapper
@@ -0,0 +1,62 @@
'use client'
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs'
import { pestDetectionDomainService } from '@/libs/api/services/pestDetectionDomainService'
import type { PestRiskSummary } from '@/libs/api/services/pestDetectionDomainService'
const PestRiskPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [data, setData] = useState<PestRiskSummary>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
pestDetectionDomainService
.getRiskSummary(farmUuid)
.then(summary => setData(summary ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
{data.disease_risk && (
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.disease_risk as Record<string, unknown>} />
</Grid>
)}
{data.pest_risk && (
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.pest_risk as Record<string, unknown>} />
</Grid>
)}
</Grid>
</Box>
)
}
export default PestRiskPageWrapper
@@ -1,31 +1,19 @@
'use client'
// React Imports
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
// MUI Imports
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
// Component Imports
import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap'
import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart'
import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart'
import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard'
// Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
/** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
const SOIL_ROWS: CardId[][] = [
['soilMoistureHeatmap'],
['sensorValuesList', 'sensorRadarChart'],
['sensorComparisonChart', 'anomalyDetectionCard']
]
import { soilService } from '@/libs/api/services/soilService'
import type { SoilSummary } from '@/libs/api/services/soilService'
const cardRowSx = {
display: 'flex',
@@ -34,32 +22,24 @@ const cardRowSx = {
'& > *': { flex: 1, minHeight: 0 }
}
const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Record<string, unknown> }>>> = {
soilMoistureHeatmap: SoilMoistureHeatmap,
sensorValuesList: SensorValuesList,
sensorComparisonChart: SensorComparisonChart,
sensorRadarChart: SensorRadarChart,
anomalyDetectionCard: AnomalyDetectionCard
}
const SoilDataDashboardWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [data, setData] = useState<SoilSummary>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setCardsData({})
setData({})
setLoading(false)
return
}
setLoading(true)
farmDashboardService
.getAllCards(farmUuid)
.then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({}))
soilService
.getSummary(farmUuid)
.then(summary => setData(summary ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
@@ -74,28 +54,20 @@ const SoilDataDashboardWrapper = () => {
return (
<Box position='relative'>
<Grid container spacing={6}>
{SOIL_ROWS.map((rowCards, rowIndex) => {
const sizePerCard = 12 / rowCards.length
return (
<Grid
key={rowIndex}
size={12}
container
spacing={6}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
{rowCards.map(cardId => {
const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
<Grid size={12} sx={cardRowSx}>
<SoilMoistureHeatmap data={data.soilMoistureHeatmap as Record<string, unknown>} />
</Grid>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'stretch' }}>
<Grid size={{ xs: 12, lg: 7 }} sx={cardRowSx}>
<SensorRadarChart data={data.sensorRadarChart as Record<string, unknown>} />
</Grid>
<Grid size={{ xs: 12, lg: 5 }} sx={cardRowSx}>
<AnomalyDetectionCard data={data.anomalyDetectionCard as Record<string, unknown>} />
</Grid>
</Grid>
<Grid size={12} sx={cardRowSx}>
<SensorComparisonChart data={data.sensorComparisonChart as Record<string, unknown>} />
</Grid>
</Grid>
</Box>
)
@@ -11,19 +11,11 @@ import CircularProgress from '@mui/material/CircularProgress'
// Component Imports
import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard'
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction'
import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs'
// Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
/** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
const WATER_ROWS: CardId[][] = [
['farmWeatherCard', 'farmAlertsTimeline', 'waterNeedPrediction'],
['sensorValuesList']
]
import { waterService } from '@/libs/api/services/waterService'
const cardRowSx = {
display: 'flex',
@@ -32,31 +24,24 @@ const cardRowSx = {
'& > *': { flex: 1, minHeight: 0 }
}
const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Record<string, unknown> }>>> = {
farmWeatherCard: FarmWeatherCard,
farmAlertsTimeline: FarmAlertsTimeline,
waterNeedPrediction: WaterNeedPrediction,
sensorValuesList: SensorValuesList
}
const WaterDataDashboardWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [data, setData] = useState<Record<string, unknown>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setCardsData({})
setData({})
setLoading(false)
return
}
setLoading(true)
farmDashboardService
.getAllCards(farmUuid)
.then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({}))
waterService
.getSummary(farmUuid)
.then(summary => setData((summary as Record<string, unknown>) ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
@@ -71,28 +56,19 @@ const WaterDataDashboardWrapper = () => {
return (
<Box position='relative'>
<Grid container spacing={6}>
{WATER_ROWS.map((rowCards, rowIndex) => {
const sizePerCard = 12 / rowCards.length
return (
<Grid
key={rowIndex}
size={12}
container
spacing={6}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
{rowCards.map(cardId => {
const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
{data.water_stress_index != null && (
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.water_stress_index as Record<string, unknown>} />
</Grid>
)}
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'stretch' }}>
<Grid size={{ xs: 12, md: 6 }} sx={cardRowSx}>
<FarmWeatherCard data={data.farmWeatherCard as Record<string, unknown>} />
</Grid>
<Grid size={{ xs: 12, md: 6 }} sx={cardRowSx}>
<WaterNeedPrediction data={data.waterNeedPrediction as Record<string, unknown>} />
</Grid>
</Grid>
</Grid>
</Box>
)
@@ -0,0 +1,74 @@
'use client'
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import HarvestPredictionCard from '@views/dashboards/farm/HarvestPredictionCard'
import YieldPredictionChart from '@views/dashboards/farm/YieldPredictionChart'
import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs'
import { yieldHarvestService } from '@/libs/api/services/yieldHarvestService'
import type { YieldHarvestSummary } from '@/libs/api/services/yieldHarvestService'
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
const YieldHarvestPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [data, setData] = useState<YieldHarvestSummary>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
yieldHarvestService
.getSummary(farmUuid)
.then(summary => setData(summary ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
{data.yield_prediction && (
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.yield_prediction as Record<string, unknown>} />
</Grid>
)}
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'stretch' }}>
<Grid size={{ xs: 12, md: 4 }} sx={cardRowSx}>
<HarvestPredictionCard data={data.harvestPredictionCard as Record<string, unknown>} />
</Grid>
<Grid size={{ xs: 12, md: 8 }} sx={cardRowSx}>
<YieldPredictionChart data={data.yieldPredictionChart as Record<string, unknown>} />
</Grid>
</Grid>
</Grid>
</Box>
)
}
export default YieldHarvestPageWrapper