diff --git a/messages/fa.json b/messages/fa.json
index 7a1b4dc..810f4e8 100644
--- a/messages/fa.json
+++ b/messages/fa.json
@@ -152,6 +152,7 @@
"farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی",
+ "farmCalendar": "تقویم کشاورز",
"sensorSection": "سنسورها",
"sensor7In1": "سنسور خاک 7 در 1"
},
diff --git a/src/app/(dashboard)/(private)/crop-health/page.tsx b/src/app/(dashboard)/(private)/crop-health/page.tsx
deleted file mode 100644
index 72e4fae..0000000
--- a/src/app/(dashboard)/(private)/crop-health/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import CropHealthPageWrapper from '@views/dashboards/farm/CropHealthPageWrapper'
-
-const CropHealthPage = async () => {
- return
-}
-
-export default CropHealthPage
diff --git a/src/app/(dashboard)/(private)/economic-overview/page.tsx b/src/app/(dashboard)/(private)/economic-overview/page.tsx
deleted file mode 100644
index 226d183..0000000
--- a/src/app/(dashboard)/(private)/economic-overview/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import EconomicOverviewPageWrapper from '@views/dashboards/farm/EconomicOverviewPageWrapper'
-
-const EconomicOverviewPage = async () => {
- return
-}
-
-export default EconomicOverviewPage
diff --git a/src/app/(dashboard)/(private)/farmer-calendar/page.tsx b/src/app/(dashboard)/(private)/farmer-calendar/page.tsx
new file mode 100644
index 0000000..3e4cf4e
--- /dev/null
+++ b/src/app/(dashboard)/(private)/farmer-calendar/page.tsx
@@ -0,0 +1,7 @@
+import FarmerCalendarPage from '@views/dashboards/farm/FarmerCalendarPage'
+
+const FarmerCalendar = async () => {
+ return
+}
+
+export default FarmerCalendar
diff --git a/src/app/(dashboard)/(private)/yield-harvest/UI_DESCRIPTION.md b/src/app/(dashboard)/(private)/yield-harvest/UI_DESCRIPTION.md
new file mode 100644
index 0000000..4e7bbdf
--- /dev/null
+++ b/src/app/(dashboard)/(private)/yield-harvest/UI_DESCRIPTION.md
@@ -0,0 +1,409 @@
+# Yield & Harvest UI Documentation
+
+## Overview
+
+The `yield-harvest` page is designed as a two-part production dashboard for the farm domain:
+
+1. **Interactive plant simulation section** for visual learning and scenario testing.
+2. **Yield and harvest analytics section** for practical production insights and KPI-based monitoring.
+
+This page is rendered from `src/app/(dashboard)/(private)/yield-harvest/page.tsx` and uses `PlantProductionPage` as the top-level UI container.
+
+---
+
+## Page Structure
+
+The page is vertically stacked and contains two full-width blocks:
+
+### 1. Plant Simulator Block
+Located at the top of the page.
+
+- Full-width section inside a responsive grid.
+- Intended to simulate plant growth visually and numerically.
+- Uses a split layout:
+ - **Left column:** animated plant visualization and interactive controls.
+ - **Right column:** growth charts, progress indicators, and explanatory text.
+
+### 2. Yield & Harvest Analytics Block
+Located below the simulator.
+
+- Full-width analytics area.
+- Includes:
+ - KPI cards for yield-related summary numbers.
+ - Harvest prediction card.
+ - Yield prediction line chart.
+
+This layout creates a natural flow from **simulation -> monitoring -> prediction**.
+
+---
+
+## Detailed UI Breakdown
+
+## A. Plant Simulator Section
+
+Source: `src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx`
+
+### A.1 Section Header
+- Large centered title with a plant emoji.
+- Strong visual emphasis to frame the simulator as the hero feature of the page.
+- Gives the page a more experimental and educational feel before the business analytics area.
+
+### A.2 Left Column: Visual Plant Panel
+This side focuses on the animated growth representation.
+
+#### Main visualization card
+- White card container with centered content.
+- Contains an SVG-based animated plant.
+- The plant evolves over time based on internal state:
+ - height
+ - leaves
+ - branches
+ - fruits
+ - yield
+ - yield rate
+
+#### Stats mini-cards under the plant
+A responsive grid of small statistic cards shows the live simulation state:
+
+- Plant height
+- Leaf count
+- Branch count
+- Fruit count
+- Total yield
+- Yield rate
+
+UI characteristics:
+- Compact outlined cards
+- Numeric value on top
+- Label below
+- Different semantic colors for different metrics
+
+#### Max growth state
+- When the plant reaches maximum growth, a warning-colored animated status text appears.
+- This creates a clear end-state in the simulation flow.
+
+### A.3 Left Column: Controls Card
+Below the visualization card there is a separate control panel.
+
+#### Action buttons
+- **Start / Stop** button
+ - Changes color depending on state.
+ - Green when ready to start.
+ - Red when running.
+- **Reset** button
+ - Secondary outlined style.
+ - Stops simulation and resets all values.
+
+#### Sliders / range controls
+Three control groups are shown as simple horizontal sliders:
+
+- Growth speed
+- Light percentage
+- Water percentage
+
+Each control has:
+- left-aligned label
+- right-aligned current numeric value
+- full-width slider below
+
+This gives the simulator a lightweight lab-tool feel.
+
+#### Effective rate box
+- Small outlined summary card at the bottom of controls.
+- Shows the calculated effective growth rate.
+- Important because it explains how environment and speed combine into the final growth behavior.
+
+### A.4 Right Column: Growth Chart Panel
+The right side is a large analytics card for simulation history.
+
+#### Main chart area
+- Displays history for:
+ - height
+ - leaves
+ - yield
+ - yield rate
+- This section is the analytical companion to the animated plant.
+- Helps the user compare visual growth with numeric changes over time.
+
+#### Progress cards row
+Below the chart there are four compact progress status cards:
+
+- Growth progress
+- Light status
+- Water status
+- Yield status
+
+Each card includes:
+- a small title
+- a progress bar
+- a highlighted numeric value
+
+This row acts as a quick operational summary for the simulator.
+
+#### Explanatory description card
+- A final outlined text card explains the behavior of the simulator.
+- Works as a help/description area and lowers cognitive load for first-time users.
+
+### A.5 UX Role of the Simulator
+The simulator is not only decorative; it shapes the page identity.
+
+Its UI purpose is to:
+- create a more engaging entry point for the user
+- support experimentation with light/water/growth speed
+- visually communicate crop development
+- make the production dashboard feel more intelligent and interactive
+
+---
+
+## B. Yield & Harvest Analytics Section
+
+Source: `src/views/dashboards/farm/YieldHarvestPageWrapper.tsx`
+
+This section is data-driven and loads summary information based on the selected farm.
+
+### B.1 Loading State
+Before content is available:
+- a centered circular loader is shown
+- the layout remains minimal and clean
+- this avoids rendering broken cards before farm data is ready
+
+### B.2 KPI Row
+Source component: `src/views/dashboards/farm/FarmOverviewKPIs.tsx`
+
+If yield prediction KPI data exists, a responsive KPI row is displayed.
+
+#### KPI card behavior
+- Each KPI is rendered as a vertical statistics card.
+- Cards adapt to the number of items.
+- Common information shown in each card:
+ - title
+ - subtitle
+ - main statistic
+ - avatar icon
+ - avatar color
+ - optional chip badge
+
+#### Visual style
+- Dashboard-style summary cards
+- Strong scanability
+- Good at communicating top-level performance in one glance
+
+#### UX purpose
+This row acts as the executive summary of the page before the user goes deeper into charts and detailed predictions.
+
+### B.3 Bottom Analytics Row
+This row is split into two cards:
+
+- **Left:** Harvest prediction card
+- **Right:** Yield prediction chart
+
+The ratio is approximately:
+- 4 columns for harvest prediction
+- 8 columns for chart insight
+
+This gives the chart more space while preserving a compact side card for the date-based prediction.
+
+---
+
+## C. Harvest Prediction Card
+
+Source: `src/views/dashboards/farm/HarvestPredictionCard.tsx`
+
+### C.1 Card header
+The header includes:
+- success-colored avatar with calendar icon
+- title for harvest prediction
+- AI-estimated date subheader
+- option menu on the right
+
+The option menu suggests actions such as:
+- details
+- adjust
+- export
+
+This makes the card feel actionable rather than static.
+
+### C.2 Main content
+The content is vertically spaced and intentionally minimal.
+
+It includes:
+- large harvest date text as the focal point
+- optional chip showing remaining days until harvest
+- secondary description text explaining the context
+
+### C.3 UI role
+This card is designed to answer one critical user question fast:
+
+**"When is the expected harvest date?"**
+
+Because of that, the hierarchy is strong:
+- date first
+- countdown second
+- explanation third
+
+---
+
+## D. Yield Prediction Chart
+
+Source: `src/views/dashboards/farm/YieldPredictionChart.tsx`
+
+### D.1 Card header
+The chart card header includes:
+- title for yield prediction chart
+- subheader comparing current year vs previous year
+- option menu with actions like export, compare, and details
+
+This makes the chart feel like a decision-support tool.
+
+### D.2 Chart body
+- Uses an Apex line chart.
+- Smooth line strokes.
+- Top legend.
+- Two major comparison colors:
+ - primary
+ - success
+- Dashed grid lines for a cleaner analytics look.
+- X-axis categories typically represent months.
+- Y-axis values are formatted in tons.
+- Tooltip values are also formatted in tons.
+
+### D.3 Summary list under the chart
+If summary items exist, they appear below the chart as stacked summary rows.
+
+Each row includes:
+- rounded avatar with icon
+- title
+- subtitle
+- highlighted amount value on the far side
+
+This creates a hybrid layout:
+- chart for trend analysis
+- summary rows for quick interpretation
+
+### D.4 UI role
+This component gives the page its strongest analytical identity.
+It helps answer:
+- How is production trending?
+- How does this year compare to another reference line?
+- What supporting summary metrics matter most?
+
+---
+
+## Responsive Behavior
+
+The page is designed with responsive grid behavior.
+
+### On large screens
+- Plant simulator is split into left/right columns.
+- Harvest card and yield chart sit side by side.
+- KPI cards spread horizontally.
+
+### On medium and small screens
+- Cards stack vertically.
+- Harvest prediction moves above the chart.
+- Simulator sections become more linear and easier to scroll.
+- KPI cards adapt their width based on count.
+
+This keeps the page usable on desktop, tablet, and mobile.
+
+---
+
+## Visual Hierarchy Summary
+
+The UI hierarchy of `yield-harvest` is:
+
+1. **Interactive growth simulator** as the experiential top section
+2. **KPI summary cards** for immediate production status
+3. **Harvest date card** for the key operational decision
+4. **Yield trend chart** for deeper forecasting insight
+
+This structure is effective because it balances:
+- exploration
+- summary
+- actionability
+- forecasting
+
+---
+
+## UX Strengths of the Current Page
+
+### Strong points
+- Combines simulation and analytics in one page
+- Good mix of visual and numeric feedback
+- Clear card-based dashboard structure
+- Strong use of progressive disclosure
+- Easy scanning of important information
+- Responsive composition with logical stacking
+
+### Product feeling
+The page feels like a mix of:
+- agronomy lab tool
+- farm operations dashboard
+- lightweight forecasting center
+
+---
+
+## Suggested Future UI Improvements
+
+If this page is expanded later, the following improvements would fit naturally:
+
+### 1. Section labels or anchors
+Add explicit section separators such as:
+- Plant Simulation
+- Production Summary
+- Harvest Forecast
+
+### 2. Farm-specific header
+A top hero strip could include:
+- selected farm name
+- crop type
+- current season
+- last updated time
+
+### 3. Chart filters
+Add filters above the yield chart for:
+- crop type
+- year range
+- unit switching
+- predicted vs actual mode
+
+### 4. Harvest readiness indicator
+A radial progress or readiness gauge near the harvest prediction date could improve urgency perception.
+
+### 5. Scenario presets in simulator
+Quick buttons such as:
+- Low water
+- High sunlight
+- Optimal growth
+would make the simulator easier to test.
+
+---
+
+## File Map
+
+Main page entry:
+- `src/app/(dashboard)/(private)/yield-harvest/page.tsx`
+
+Top-level page composition:
+- `src/views/dashboards/farm/PlantProductionPage.tsx`
+
+Simulator:
+- `src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx`
+
+Yield & harvest analytics wrapper:
+- `src/views/dashboards/farm/YieldHarvestPageWrapper.tsx`
+
+Analytics components:
+- `src/views/dashboards/farm/FarmOverviewKPIs.tsx`
+- `src/views/dashboards/farm/HarvestPredictionCard.tsx`
+- `src/views/dashboards/farm/YieldPredictionChart.tsx`
+
+---
+
+## Final Summary
+
+The `yield-harvest` page UI is a full production intelligence screen.
+
+It starts with an interactive, highly visual plant-growth simulator and then transitions into a more standard analytics dashboard for yield and harvest planning. The page is especially strong in turning agricultural concepts into understandable interface blocks: simulation, KPIs, predicted date, and long-term yield trend.
+
+In short, the UI is not just a reporting surface; it is a combined **simulation + monitoring + forecasting** experience for farm production.
diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx
index 7566237..e39b9af 100644
--- a/src/components/layout/vertical/VerticalMenu.tsx
+++ b/src/components/layout/vertical/VerticalMenu.tsx
@@ -95,9 +95,6 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
{t('dashboards')}
- }>
- {t('cropHealth')}
-
}>
{t('yieldHarvest')}
@@ -107,8 +104,8 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
}>
{t('pestDiseaseRisk')}
- }>
- {t('economicOverview')}
+ }>
+ {t('farmCalendar')}
diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts
index 7a49129..8cb7256 100644
--- a/src/constants/navigation.ts
+++ b/src/constants/navigation.ts
@@ -4,9 +4,16 @@ export const navigationLabels = {
farm: 'داشبورد مزرعه',
waterData: 'دیتاهای آب',
soilData: 'اطلاعات خاک',
+ cropZoning: 'زونبندی کشت',
sensorSection: 'سنسورها',
sensor7In1: 'سنسور خاک 7 در 1',
dataSection: 'بخش دادهها',
+ recommendation: 'توصیهها',
+ irrigationRecommendation: 'توصیه آبیاری',
+ fertilizationRecommendation: 'توصیه کوددهی',
+ aiAssistant: 'دستیار هوشمند',
+ farmAiAssistant: 'دستیار هوشمند مزرعه',
+ farmCalendar: 'تقویم کشاورز',
crm: 'مدیریت ارتباط با مشتری',
analytics: 'تحلیلها',
eCommerce: 'فروشگاه',
@@ -109,4 +116,3 @@ export const navigationLabels = {
menuLevel3: 'سطح منو 3',
disabledMenu: 'منوی غیرفعال'
}
-
diff --git a/src/data/dictionaries/ar.json b/src/data/dictionaries/ar.json
index 53a9f90..a3c857e 100644
--- a/src/data/dictionaries/ar.json
+++ b/src/data/dictionaries/ar.json
@@ -110,8 +110,18 @@
"farmAlerts": "تنبيهات المزرعة",
"pestDiseaseRisk": "مخاطر الآفات والأمراض",
"economicOverview": "النظرة الاقتصادية",
+ "farmCalendar": "تقويم المزارع",
+ "dataSection": "قسم البيانات",
+ "waterData": "بيانات المياه",
+ "soilData": "بيانات التربة",
+ "cropZoning": "تقسيم المحاصيل",
"sensorSection": "المستشعرات",
- "sensor7In1": "مستشعر التربة 7 في 1"
+ "sensor7In1": "مستشعر التربة 7 في 1",
+ "recommendation": "التوصيات",
+ "irrigationRecommendation": "توصية الري",
+ "fertilizationRecommendation": "توصية التسميد",
+ "aiAssistant": "المساعد الذكي",
+ "farmAiAssistant": "مساعد المزرعة الذكي"
},
"irrigation": {
"title": "توصية الري الذكية",
diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json
index 2bab722..80c282e 100644
--- a/src/data/dictionaries/en.json
+++ b/src/data/dictionaries/en.json
@@ -110,8 +110,18 @@
"farmAlerts": "Farm Alerts",
"pestDiseaseRisk": "Pest & Disease Risk",
"economicOverview": "Economic Overview",
+ "farmCalendar": "Farmer Calendar",
+ "dataSection": "Data Section",
+ "waterData": "Water Data",
+ "soilData": "Soil Data",
+ "cropZoning": "Crop Zoning",
"sensorSection": "Sensors",
- "sensor7In1": "Soil Sensor 7-in-1"
+ "sensor7In1": "Soil Sensor 7-in-1",
+ "recommendation": "Recommendations",
+ "irrigationRecommendation": "Irrigation Recommendation",
+ "fertilizationRecommendation": "Fertilization Recommendation",
+ "aiAssistant": "AI Assistant",
+ "farmAiAssistant": "Farm AI Assistant"
},
"irrigation": {
"title": "Smart Irrigation Recommendation",
diff --git a/src/data/dictionaries/fa.json b/src/data/dictionaries/fa.json
index a3f827f..44a5e43 100644
--- a/src/data/dictionaries/fa.json
+++ b/src/data/dictionaries/fa.json
@@ -110,8 +110,18 @@
"farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی",
+ "farmCalendar": "تقویم کشاورز",
+ "dataSection": "بخش دادهها",
+ "waterData": "دیتاهای آب",
+ "soilData": "اطلاعات خاک",
+ "cropZoning": "زونبندی کشت",
"sensorSection": "سنسورها",
- "sensor7In1": "سنسور خاک 7 در 1"
+ "sensor7In1": "سنسور خاک 7 در 1",
+ "recommendation": "توصیهها",
+ "irrigationRecommendation": "توصیه آبیاری",
+ "fertilizationRecommendation": "توصیه کوددهی",
+ "aiAssistant": "دستیار هوشمند",
+ "farmAiAssistant": "دستیار هوشمند مزرعه"
},
"irrigation": {
"title": "توصیه آبیاری هوشمند",
diff --git a/src/data/dictionaries/fr.json b/src/data/dictionaries/fr.json
index 2c4e2ca..aa7456f 100644
--- a/src/data/dictionaries/fr.json
+++ b/src/data/dictionaries/fr.json
@@ -110,8 +110,18 @@
"farmAlerts": "Alertes ferme",
"pestDiseaseRisk": "Risque ravageurs et maladies",
"economicOverview": "Aperçu économique",
+ "farmCalendar": "Calendrier de l'agriculteur",
+ "dataSection": "Section des données",
+ "waterData": "Données sur l'eau",
+ "soilData": "Données du sol",
+ "cropZoning": "Zonage des cultures",
"sensorSection": "Capteurs",
- "sensor7In1": "Capteur de sol 7-en-1"
+ "sensor7In1": "Capteur de sol 7-en-1",
+ "recommendation": "Recommandations",
+ "irrigationRecommendation": "Recommandation d'irrigation",
+ "fertilizationRecommendation": "Recommandation de fertilisation",
+ "aiAssistant": "Assistant intelligent",
+ "farmAiAssistant": "Assistant IA agricole"
},
"irrigation": {
"title": "Recommandation intelligente d'irrigation",
diff --git a/src/data/navigation/horizontalMenuData.tsx b/src/data/navigation/horizontalMenuData.tsx
index d5e73f7..c3302d4 100644
--- a/src/data/navigation/horizontalMenuData.tsx
+++ b/src/data/navigation/horizontalMenuData.tsx
@@ -45,11 +45,6 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
icon: 'tabler-dashboard',
href: '/dashboard'
},
- {
- label: 'cropHealth',
- icon: 'tabler-plant',
- href: '/crop-health'
- },
{
label: 'waterWeather',
icon: 'tabler-droplet',
@@ -76,9 +71,9 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
href: '/pest-risk'
},
{
- label: 'economicOverview',
- icon: 'tabler-currency-dollar',
- href: '/economic-overview'
+ label: 'farmCalendar',
+ icon: 'tabler-calendar-event',
+ href: '/farmer-calendar'
},
{
label: 'cropZoning',
@@ -101,11 +96,6 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
icon: 'tabler-dashboard',
href: '/dashboard'
},
- {
- label: 'cropHealth',
- icon: 'tabler-plant',
- href: '/crop-health'
- },
{
label: 'waterWeather',
icon: 'tabler-droplet',
@@ -132,9 +122,9 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
href: '/pest-risk'
},
{
- label: 'economicOverview',
- icon: 'tabler-currency-dollar',
- href: '/economic-overview'
+ label: 'farmCalendar',
+ icon: 'tabler-calendar-event',
+ href: '/farmer-calendar'
},
{
label: 'cropZoning',
diff --git a/src/data/navigation/verticalMenuData.tsx b/src/data/navigation/verticalMenuData.tsx
index 40110cd..8d8ee7b 100644
--- a/src/data/navigation/verticalMenuData.tsx
+++ b/src/data/navigation/verticalMenuData.tsx
@@ -49,11 +49,6 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
icon: 'tabler-dashboard',
href: '/dashboard'
},
- {
- label: 'cropHealth',
- icon: 'tabler-plant',
- href: '/crop-health'
- },
{
label: 'waterWeather',
icon: 'tabler-droplet',
@@ -80,9 +75,9 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
href: '/pest-risk'
},
{
- label: 'economicOverview',
- icon: 'tabler-currency-dollar',
- href: '/economic-overview'
+ label: 'farmCalendar',
+ icon: 'tabler-calendar-event',
+ href: '/farmer-calendar'
},
{
label: 'cropZoning',
diff --git a/src/libs/api/services/farmAlertsService.ts b/src/libs/api/services/farmAlertsService.ts
index 0fb6b3e..41187be 100644
--- a/src/libs/api/services/farmAlertsService.ts
+++ b/src/libs/api/services/farmAlertsService.ts
@@ -2,10 +2,73 @@ import { apiClient } from '../client'
const PREFIX = '/api/farm-alerts'
-export interface FarmAlertsSummary {
- tracker?: Record
- timeline?: Record
- recommendations?: Record
+export interface FarmAlertRequestItem {
+ alert_id: string
+ level: string
+ title: string
+ message: string
+ suggested_action?: string
+ source_metric_type?: string
+ timestamp?: string | null
+ payload?: Record
+}
+
+export interface FarmAlertTrackerItem {
+ metric_type?: string
+ title?: string
+ current_value?: number
+ threshold_value?: number
+ severity?: string
+ duration?: string
+ timestamp?: string
+ domain?: string
+ unit?: string
+ icon?: string
+ summary?: string
+ recommended_action?: string
+ explanation?: string
+}
+
+export interface FarmAlertNotificationItem {
+ id: number
+ uuid: string
+ farm_uuid: string
+ since_id: number
+ endpoint?: string
+ title: string
+ message: string
+ level: string
+ suggested_action?: string
+ source_alert_id?: string
+ source_metric_type?: string
+ payload?: Record
+ is_read: boolean
+ metadata?: Record
+ created_at: string
+ updated_at?: string
+}
+
+export interface FarmAlertsTrackerPayload {
+ totalAlerts?: number
+ alerts?: FarmAlertTrackerItem[]
+ alertStats?: Array>
+ alertClusters?: Array>
+ mostCriticalIssue?: FarmAlertTrackerItem | null
+ prioritizedAlertSummaries?: string[]
+ recommendedOperationalActions?: string[]
+ humanReadableExplanations?: string[]
+}
+
+export interface FarmAlertsTrackerResponse {
+ farm_uuid: string
+ service_id: string
+ tracker: FarmAlertsTrackerPayload
+ headline?: string
+ overview?: string
+ status_level?: string
+ notifications?: FarmAlertNotificationItem[]
+ raw_llm_response?: string
+ structured_context?: Record
}
interface ApiResponse {
@@ -25,9 +88,19 @@ function extract(res: ApiResponse | T): T {
}
export const farmAlertsService = {
- async getTracker(farmUuid: string): Promise> {
- const res = await apiClient.get> | Record>(
- `${PREFIX}/tracker/?farm_uuid=${encodeURIComponent(farmUuid)}`
+ async analyzeTracker(
+ payload: { farmUuid: string; alerts?: FarmAlertRequestItem[] },
+ ): Promise {
+ const requestBody = {
+ farm_uuid: payload.farmUuid,
+ ...(payload.alerts?.length ? { alerts: payload.alerts } : {}),
+ }
+
+ const res = await apiClient.post<
+ ApiResponse | FarmAlertsTrackerResponse
+ >(
+ `${PREFIX}/tracker/`,
+ requestBody,
)
return extract(res)
},
diff --git a/src/libs/styles/AppFullCalendar.ts b/src/libs/styles/AppFullCalendar.ts
index 804f11c..ea8e7e9 100644
--- a/src/libs/styles/AppFullCalendar.ts
+++ b/src/libs/styles/AppFullCalendar.ts
@@ -10,6 +10,7 @@ const AppFullCalendar = styled('div')(({ theme }: { theme: Theme }) => ({
position: 'relative',
borderRadius: 'var(--mui-shape-borderRadius)',
'& .fc': {
+ width: '100%',
zIndex: 1,
'.fc-col-header, .fc-daygrid-body, .fc-scrollgrid-sync-table, .fc-timegrid-body, .fc-timegrid-body table': {
diff --git a/src/views/apps/calendar/Calendar.tsx b/src/views/apps/calendar/Calendar.tsx
index 2647725..6c6ae71 100644
--- a/src/views/apps/calendar/Calendar.tsx
+++ b/src/views/apps/calendar/Calendar.tsx
@@ -27,9 +27,13 @@ type CalenderProps = {
calendarApi: any
setCalendarApi: (val: any) => void
calendarsColor: CalendarColors
- dispatch: AppDispatch
- handleLeftSidebarToggle: () => void
- handleAddEventSidebarToggle: () => void
+ dispatch?: AppDispatch
+ handleLeftSidebarToggle?: () => void
+ handleAddEventSidebarToggle?: () => void
+ editable?: boolean
+ showSidebarToggle?: boolean
+ onDateClick?: (date: Date) => void
+ onEventClick?: (event: any) => void
}
const blankEvent: AddEventType = {
@@ -54,7 +58,11 @@ const Calendar = (props: CalenderProps) => {
calendarsColor,
dispatch,
handleAddEventSidebarToggle,
- handleLeftSidebarToggle
+ handleLeftSidebarToggle,
+ editable = true,
+ showSidebarToggle = true,
+ onDateClick,
+ onEventClick
} = props
// Refs
@@ -78,7 +86,7 @@ const Calendar = (props: CalenderProps) => {
initialView: 'dayGridMonth',
locale: 'fa',
headerToolbar: {
- start: 'sidebarToggle,prev,next,title',
+ start: `${showSidebarToggle ? 'sidebarToggle,' : ''}prev,next,title`,
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
},
views: {
@@ -153,7 +161,7 @@ const Calendar = (props: CalenderProps) => {
Enable dragging and resizing event
? Docs: https://fullcalendar.io/docs/editable
*/
- editable: true,
+ editable,
/*
Enable resizing event from start
@@ -192,8 +200,12 @@ const Calendar = (props: CalenderProps) => {
eventClick({ event: clickedEvent, jsEvent }: any) {
jsEvent.preventDefault()
- dispatch(selectedEvent(clickedEvent))
- handleAddEventSidebarToggle()
+ onEventClick?.(clickedEvent)
+
+ if (dispatch && handleAddEventSidebarToggle) {
+ dispatch(selectedEvent(clickedEvent))
+ handleAddEventSidebarToggle()
+ }
if (clickedEvent.url) {
// Open the URL in a new tab
@@ -210,12 +222,18 @@ const Calendar = (props: CalenderProps) => {
sidebarToggle: {
icon: 'tabler tabler-menu-2',
click() {
- handleLeftSidebarToggle()
+ handleLeftSidebarToggle?.()
}
}
},
dateClick(info: any) {
+ onDateClick?.(info.date)
+
+ if (!dispatch || !handleAddEventSidebarToggle) {
+ return
+ }
+
const ev = { ...blankEvent }
ev.start = info.date
@@ -232,6 +250,10 @@ const Calendar = (props: CalenderProps) => {
? We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
*/
eventDrop({ event: droppedEvent }: any) {
+ if (!dispatch) {
+ return
+ }
+
// Convert FullCalendar event to API format
const eventData = {
start: droppedEvent.start ? new Date(droppedEvent.start).toISOString() : '',
@@ -247,6 +269,10 @@ const Calendar = (props: CalenderProps) => {
? Docs: https://fullcalendar.io/docs/eventResize
*/
eventResize({ event: resizedEvent }: any) {
+ if (!dispatch) {
+ return
+ }
+
// Convert FullCalendar event to API format
const eventData = {
start: resizedEvent.start ? new Date(resizedEvent.start).toISOString() : '',
diff --git a/src/views/dashboards/farm/AlertsPageWrapper.tsx b/src/views/dashboards/farm/AlertsPageWrapper.tsx
index b4360a6..9606b60 100644
--- a/src/views/dashboards/farm/AlertsPageWrapper.tsx
+++ b/src/views/dashboards/farm/AlertsPageWrapper.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
+import { format } from 'date-fns'
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
@@ -9,9 +10,15 @@ import CircularProgress from '@mui/material/CircularProgress'
import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker'
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
+import NotificationSettingsCard from '@views/dashboards/farm/NotificationSettingsCard'
import RecommendationsList from '@views/dashboards/farm/RecommendationsList'
-import { farmAlertsService } from '@/libs/api/services/farmAlertsService'
+import {
+ farmAlertsService,
+ type FarmAlertsTrackerResponse,
+ type FarmAlertTrackerItem,
+ type FarmAlertNotificationItem
+} from '@/libs/api/services/farmAlertsService'
const cardRowSx = {
display: 'flex',
@@ -20,6 +27,94 @@ const cardRowSx = {
'& > *': { flex: 1, minHeight: 0 }
}
+const getSeverityColor = (value?: string): 'primary' | 'warning' | 'error' | 'info' | 'success' => {
+ switch (value?.toLowerCase()) {
+ case 'critical':
+ case 'danger':
+ case 'error':
+ case 'high':
+ return 'error'
+ case 'warning':
+ case 'medium':
+ return 'warning'
+ case 'success':
+ return 'success'
+ case 'low':
+ case 'info':
+ default:
+ return 'info'
+ }
+}
+
+const buildTrackerCardData = (result: FarmAlertsTrackerResponse): Record => {
+ const tracker = result.tracker ?? {}
+ const totalAlerts = tracker.totalAlerts ?? 0
+ const alertStats = Array.isArray(tracker.alertStats) ? tracker.alertStats : []
+ const safeTotal = Math.max(totalAlerts, 1)
+ const criticalCount = (alertStats as Array>).reduce((sum, item) => {
+ const severity = String(item.severity ?? '').toLowerCase()
+
+ return severity === 'high' || severity === 'critical' || severity === 'danger' ? sum + Number(item.count ?? 0) : sum
+ }, 0)
+
+ return {
+ totalAlerts,
+ alertStats: alertStats.map((item, index) => ({
+ title: String(item.title ?? `Alert ${index + 1}`),
+ count: String(item.count ?? '0'),
+ avatarIcon: String(item.avatarIcon ?? 'tabler-alert-triangle'),
+ avatarColor: getSeverityColor(String(item.severity ?? item.avatarColor ?? result.status_level)),
+ })),
+ radialBarValue: Math.min(Math.round((criticalCount / safeTotal) * 100), 100),
+ }
+}
+
+const buildTimelineData = (
+ result: FarmAlertsTrackerResponse,
+ notifications: FarmAlertNotificationItem[],
+): Record => {
+ const trackerAlerts = Array.isArray(result.tracker?.alerts) ? result.tracker.alerts : []
+
+ if (notifications.length > 0) {
+ return {
+ alerts: notifications.map(item => ({
+ title: item.title,
+ description: item.suggested_action || item.message,
+ time: format(new Date(item.created_at), 'yyyy-MM-dd HH:mm'),
+ color: getSeverityColor(item.level),
+ })),
+ }
+ }
+
+ return {
+ alerts: trackerAlerts.map((item: FarmAlertTrackerItem, index: number) => ({
+ title: item.title || `Alert ${index + 1}`,
+ description: item.explanation || item.summary || item.recommended_action || '',
+ time: item.duration || (item.timestamp ? format(new Date(item.timestamp), 'yyyy-MM-dd HH:mm') : '-'),
+ color: getSeverityColor(item.severity || result.status_level),
+ })),
+ }
+}
+
+const buildRecommendationsData = (result: FarmAlertsTrackerResponse): Record => {
+ const tracker = result.tracker ?? {}
+ const actions = Array.isArray(tracker.recommendedOperationalActions)
+ ? tracker.recommendedOperationalActions
+ : []
+ const explanations = Array.isArray(tracker.humanReadableExplanations)
+ ? tracker.humanReadableExplanations
+ : []
+
+ return {
+ recommendations: actions.map((action, index) => ({
+ title: `اقدام پیشنهادی ${index + 1}`,
+ subtitle: explanations[index] || action,
+ avatarIcon: 'tabler-arrow-up-right',
+ avatarColor: getSeverityColor(result.status_level),
+ })),
+ }
+}
+
const AlertsPageWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
@@ -38,15 +133,19 @@ const AlertsPageWrapper = () => {
}
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)
+ farmAlertsService
+ .analyzeTracker({ farmUuid })
+ .then(result => {
+ const notifications = Array.isArray(result.notifications) ? result.notifications : []
+
+ setTracker(buildTrackerCardData(result))
+ setTimeline(buildTimelineData(result, notifications))
+ setRecommendations(buildRecommendationsData(result))
+ })
+ .catch(() => {
+ setTracker({})
+ setTimeline({})
+ setRecommendations({})
})
.finally(() => setLoading(false))
}, [farmUuid])
@@ -73,6 +172,9 @@ const AlertsPageWrapper = () => {
+
+
+
)
diff --git a/src/views/dashboards/farm/CropHealthPageWrapper.tsx b/src/views/dashboards/farm/CropHealthPageWrapper.tsx
deleted file mode 100644
index 73fe6f6..0000000
--- a/src/views/dashboards/farm/CropHealthPageWrapper.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-'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>({})
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- if (!farmUuid) {
- setData({})
- setLoading(false)
- return
- }
-
- setLoading(true)
- cropHealthService
- .getSummary(farmUuid)
- .then(summary => setData((summary as Record) ?? {}))
- .catch(() => setData({}))
- .finally(() => setLoading(false))
- }, [farmUuid])
-
- if (loading) {
- return (
-
-
-
- )
- }
-
- return (
-
-
-
- } />
-
-
- } />
-
-
-
- )
-}
-
-export default CropHealthPageWrapper
diff --git a/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx b/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx
deleted file mode 100644
index 6c0d233..0000000
--- a/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-'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({})
- 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 (
-
-
-
- )
- }
-
- return (
-
-
-
- } />
-
-
-
- )
-}
-
-export default EconomicOverviewPageWrapper
diff --git a/src/views/dashboards/farm/FarmerCalendarPage.tsx b/src/views/dashboards/farm/FarmerCalendarPage.tsx
new file mode 100644
index 0000000..52e42e9
--- /dev/null
+++ b/src/views/dashboards/farm/FarmerCalendarPage.tsx
@@ -0,0 +1,486 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+
+import type { EventInput } from '@fullcalendar/core'
+
+import Box from '@mui/material/Box'
+import Card from '@mui/material/Card'
+import CardContent from '@mui/material/CardContent'
+import Chip from '@mui/material/Chip'
+import Divider from '@mui/material/Divider'
+import Grid from '@mui/material/Grid2'
+import Stack from '@mui/material/Stack'
+import Typography from '@mui/material/Typography'
+
+import AppFullCalendar from '@/libs/styles/AppFullCalendar'
+import Calendar from '@views/apps/calendar/Calendar'
+import type { ThemeColor } from '@/@core/types'
+import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
+
+const calendarColors: CalendarColors = {
+ Personal: 'success',
+ Business: 'warning',
+ Family: 'primary',
+ Holiday: 'info',
+ ETC: 'error'
+}
+
+const dayFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long'
+})
+
+const fullDateFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric'
+})
+
+const makeDate = (year: number, month: number, day: number, hour = 8, minute = 0) =>
+ new Date(year, month, day, hour, minute)
+
+const today = new Date()
+const year = today.getFullYear()
+const month = today.getMonth()
+
+const farmerEvents: EventInput[] = [
+ {
+ id: 'irrigation-1',
+ title: 'آبیاری قطعه شمالی',
+ start: makeDate(year, month, 4, 6, 30).toISOString(),
+ end: makeDate(year, month, 4, 8, 0).toISOString(),
+ extendedProps: {
+ calendar: 'Personal',
+ description: 'آبیاری قطره ای برای گوجه فرنگی ها با بررسی فشار خطوط قبل از شروع.'
+ }
+ },
+ {
+ id: 'nutrition-1',
+ title: 'کوددهی مزرعه ذرت',
+ start: makeDate(year, month, 6, 9, 0).toISOString(),
+ end: makeDate(year, month, 6, 11, 30).toISOString(),
+ extendedProps: {
+ calendar: 'Business',
+ description: 'محلول پاشی مرحله رشد رویشی و ثبت مقدار مصرف برای هر هکتار.'
+ }
+ },
+ {
+ id: 'scouting-1',
+ title: 'بازدید آفات و بیماری',
+ start: makeDate(year, month, 8, 7, 30).toISOString(),
+ end: makeDate(year, month, 8, 9, 0).toISOString(),
+ extendedProps: {
+ calendar: 'ETC',
+ description: 'بررسی لکه های برگی، جمع آوری نمونه و ثبت نقاط بحرانی در مزرعه.'
+ }
+ },
+ {
+ id: 'harvest-1',
+ title: 'برداشت آزمایشی زعفران',
+ start: makeDate(year, month, 11, 5, 0).toISOString(),
+ end: makeDate(year, month, 11, 9, 0).toISOString(),
+ extendedProps: {
+ calendar: 'Family',
+ description: 'هماهنگی نیروی کار و ثبت عملکرد اولیه برای برنامه ریزی برداشت اصلی.'
+ }
+ },
+ {
+ id: 'market-1',
+ title: 'جلسه فروش با خریدار عمده',
+ start: makeDate(year, month, 14, 12, 0).toISOString(),
+ end: makeDate(year, month, 14, 13, 0).toISOString(),
+ extendedProps: {
+ calendar: 'Holiday',
+ description: 'بررسی قیمت هفتگی، کیفیت محصول و زمان بندی تحویل بار.'
+ }
+ },
+ {
+ id: 'irrigation-2',
+ title: 'شست وشوی فیلترهای آبیاری',
+ start: makeDate(year, month, 18, 6, 0).toISOString(),
+ end: makeDate(year, month, 18, 7, 0).toISOString(),
+ extendedProps: {
+ calendar: 'Personal',
+ description: 'سرویس پیشگیرانه برای جلوگیری از افت دبی در آبیاری نوبت بعد.'
+ }
+ },
+ {
+ id: 'soil-1',
+ title: 'نمونه برداری خاک',
+ start: makeDate(year, month, 20, 8, 0).toISOString(),
+ end: makeDate(year, month, 20, 10, 0).toISOString(),
+ extendedProps: {
+ calendar: 'Business',
+ description: 'نمونه گیری از سه ناحیه برای ارسال به آزمایشگاه و تنظیم نسخه تغذیه.'
+ }
+ },
+ {
+ id: 'maintenance-1',
+ title: 'سرویس تراکتور و ادوات',
+ start: makeDate(year, month, 23, 15, 0).toISOString(),
+ end: makeDate(year, month, 23, 17, 0).toISOString(),
+ extendedProps: {
+ calendar: 'ETC',
+ description: 'تعویض فیلتر روغن، گریس کاری و آماده سازی برای عملیات آخر هفته.'
+ }
+ }
+]
+
+const calendarStore: CalendarType = {
+ events: farmerEvents,
+ filteredEvents: farmerEvents,
+ selectedEvent: null,
+ selectedCalendars: ['Personal', 'Business', 'Family', 'Holiday', 'ETC']
+}
+
+const overviewItems = [
+ {
+ label: 'کارهای این هفته',
+ value: '۸ برنامه',
+ note: '۳ مورد نیازمند پیگیری امروز',
+ accent: 'linear-gradient(135deg, rgba(34,197,94,0.18), rgba(22,163,74,0.05))'
+ },
+ {
+ label: 'اقدام بعدی',
+ value: 'آبیاری شمال مزرعه',
+ note: 'فردا، ساعت ۶:۳۰ صبح',
+ accent: 'linear-gradient(135deg, rgba(14,165,233,0.18), rgba(2,132,199,0.05))'
+ },
+ {
+ label: 'اولویت بحرانی',
+ value: 'بازدید آفات',
+ note: 'تا ۴۸ ساعت آینده انجام شود',
+ accent: 'linear-gradient(135deg, rgba(249,115,22,0.18), rgba(234,88,12,0.05))'
+ }
+]
+
+const legendItems = [
+ { label: 'آبیاری و عملیات روزانه', color: 'success.main' },
+ { label: 'تغذیه و خاک', color: 'warning.main' },
+ { label: 'برداشت و نیروی کار', color: 'primary.main' },
+ { label: 'جلسات و هماهنگی فروش', color: 'info.main' },
+ { label: 'ریسک ها و نگهداری', color: 'error.main' }
+]
+
+const getEventDate = (event: EventInput) => {
+ const raw = event.start
+
+ if (raw instanceof Date) {
+ return raw
+ }
+
+ return raw ? new Date(raw) : new Date()
+}
+
+const formatTimeRange = (event: EventInput) => {
+ const start = getEventDate(event)
+ const end =
+ event.end instanceof Date ? event.end : event.end ? new Date(event.end) : null
+
+ const timeFormatter = new Intl.DateTimeFormat('fa-IR', {
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+
+ return end
+ ? `${timeFormatter.format(start)} تا ${timeFormatter.format(end)}`
+ : timeFormatter.format(start)
+}
+
+const getCalendarLabel = (event: EventInput) => {
+ const type = event.extendedProps?.calendar
+
+ switch (type) {
+ case 'Personal':
+ return 'آبیاری'
+ case 'Business':
+ return 'تغذیه'
+ case 'Family':
+ return 'برداشت'
+ case 'Holiday':
+ return 'بازار'
+ default:
+ return 'نگهداری'
+ }
+}
+
+const getCalendarChipColor = (event: EventInput): ThemeColor => {
+ const type = event.extendedProps?.calendar as keyof CalendarColors | undefined
+
+ return type ? calendarColors[type] : 'secondary'
+}
+
+const upcomingEvents = [...farmerEvents]
+ .sort((left, right) => getEventDate(left).getTime() - getEventDate(right).getTime())
+ .slice(0, 5)
+
+const FarmerCalendarPage = () => {
+ const [calendarApi, setCalendarApi] = useState(null)
+ const [selectedEvent, setSelectedEvent] = useState(upcomingEvents[0])
+
+ const selectedEventDate = useMemo(() => fullDateFormatter.format(getEventDate(selectedEvent)), [selectedEvent])
+
+ return (
+
+
+
+
+
+
+
+
+
+ تقویم بزرگ عملیات کشاورز برای مدیریت آبیاری، تغذیه، برداشت و کارهای روزانه
+
+
+ این صفحه تمام برنامه های مزرعه را در یک نمای ماهانه جمع می کند تا کشاورز بداند
+ امروز چه کاری مهم تر است و در روزهای آینده چه چیزی باید آماده شود.
+
+
+
+
+
+ {overviewItems.map(item => (
+
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+ {item.note}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ تقویم عملیات مزرعه
+
+ روی هر برنامه کلیک کن تا جزئیات آن در پنل کناری نمایش داده شود.
+
+
+
+ {legendItems.map(item => (
+
+
+
+ {item.label}
+
+
+ ))}
+
+
+
+
+ setSelectedEvent(event.toPlainObject())}
+ onDateClick={date => {
+ const nearest =
+ [...farmerEvents].find(event => {
+ const eventDate = getEventDate(event)
+
+ return (
+ eventDate.getFullYear() === date.getFullYear() &&
+ eventDate.getMonth() === date.getMonth() &&
+ eventDate.getDate() === date.getDate()
+ )
+ }) || selectedEvent
+
+ setSelectedEvent(nearest)
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ برنامه انتخاب شده
+
+
+ {selectedEvent.title}
+
+
+
+
+
+
+
+ تاریخ
+
+
+ {selectedEventDate}
+
+
+
+
+
+ بازه زمانی
+
+
+ {formatTimeRange(selectedEvent)}
+
+
+
+
+
+
+
+ توضیحات اجرایی
+
+
+ {selectedEvent.extendedProps?.description as string}
+
+
+
+
+
+
+
+
+
+ برنامه های نزدیک
+
+
+ {upcomingEvents.map(event => (
+ setSelectedEvent(event)}
+ sx={{
+ p: 3,
+ borderRadius: 3,
+ cursor: 'pointer',
+ border: theme => `1px solid ${theme.palette.divider}`,
+ backgroundColor:
+ selectedEvent.id === event.id ? 'action.hover' : 'background.paper',
+ transition: 'all 0.2s ease',
+ '&:hover': {
+ borderColor: 'primary.main',
+ transform: 'translateY(-2px)'
+ }
+ }}
+ >
+
+
+ {event.title}
+
+ {dayFormatter.format(getEventDate(event))}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
+
+export default FarmerCalendarPage
diff --git a/src/views/dashboards/farm/NotificationSettingsCard.tsx b/src/views/dashboards/farm/NotificationSettingsCard.tsx
new file mode 100644
index 0000000..a125a60
--- /dev/null
+++ b/src/views/dashboards/farm/NotificationSettingsCard.tsx
@@ -0,0 +1,172 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+
+import Box from '@mui/material/Box'
+import Card from '@mui/material/Card'
+import CardContent from '@mui/material/CardContent'
+import CardHeader from '@mui/material/CardHeader'
+import Chip from '@mui/material/Chip'
+import Divider from '@mui/material/Divider'
+import FormControlLabel from '@mui/material/FormControlLabel'
+import Stack from '@mui/material/Stack'
+import Switch from '@mui/material/Switch'
+import Typography from '@mui/material/Typography'
+
+export type NotificationChannelKey = 'email' | 'sms' | 'push' | 'whatsapp'
+
+export interface NotificationChannelSetting {
+ key: NotificationChannelKey
+ label: string
+ description: string
+ icon: string
+ enabled: boolean
+}
+
+interface NotificationSettingsCardProps {
+ title?: string
+ subtitle?: string
+ channels?: NotificationChannelSetting[]
+ onChange?: (channels: NotificationChannelSetting[]) => void
+}
+
+const defaultChannels: NotificationChannelSetting[] = [
+ {
+ key: 'email',
+ label: 'اعلان ایمیل',
+ description: 'هشدارهای مهم مزرعه به ایمیل مدیر ارسال شود.',
+ icon: 'tabler-mail',
+ enabled: true
+ },
+ {
+ key: 'sms',
+ label: 'اعلان پیامکی',
+ description: 'هشدارهای فوری مثل قطع آبیاری یا ریسک آفات با SMS ارسال شود.',
+ icon: 'tabler-message',
+ enabled: true
+ },
+ {
+ key: 'push',
+ label: 'اعلان داخل پنل',
+ description: 'نوتیفیکیشن ها در پنل و مرورگر نمایش داده شوند.',
+ icon: 'tabler-bell-ringing',
+ enabled: true
+ },
+ {
+ key: 'whatsapp',
+ label: 'اعلان واتساپ',
+ description: 'خلاصه هشدارهای روزانه برای گروه عملیات ارسال شود.',
+ icon: 'tabler-brand-whatsapp',
+ enabled: false
+ }
+]
+
+const NotificationSettingsCard = ({
+ title = 'تنظیمات سیستم نوتیفیکیشن',
+ subtitle = 'مشخص کن هشدارهای مزرعه از چه کانال هایی برای تیم ارسال شوند.',
+ channels = defaultChannels,
+ onChange
+}: NotificationSettingsCardProps) => {
+ const [settings, setSettings] = useState(channels)
+
+ const enabledCount = useMemo(() => settings.filter(item => item.enabled).length, [settings])
+
+ const handleToggle = (key: NotificationChannelKey) => {
+ const nextSettings = settings.map(item =>
+ item.key === key ? { ...item, enabled: !item.enabled } : item
+ )
+
+ setSettings(nextSettings)
+ onChange?.(nextSettings)
+ }
+
+ return (
+
+
+ }
+ />
+
+
+
+ {settings.map((item, index) => (
+
+ `1px solid ${theme.palette.divider}`,
+ backgroundColor: item.enabled ? 'action.hover' : 'background.paper'
+ }}
+ >
+
+
+
+
+
+
+ {item.label}
+
+ {item.description}
+
+
+
+
+ handleToggle(item.key)}
+ />
+ }
+ label={
+
+ {item.enabled ? 'فعال' : 'غیرفعال'}
+
+ }
+ labelPlacement='start'
+ />
+
+
+ {index < settings.length - 1 ? : null}
+
+ ))}
+
+
+
+ )
+}
+
+export default NotificationSettingsCard
diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx
deleted file mode 100644
index 0e847b5..0000000
--- a/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-'use client'
-
-import { useEffect, useState } from 'react'
-import { useTranslations } from 'next-intl'
-import dynamic from 'next/dynamic'
-import { useFarmHub } from '@/hooks/useFarmHub'
-
-// MUI Imports
-import Box from '@mui/material/Box'
-import Grid from '@mui/material/Grid2'
-import Card from '@mui/material/Card'
-import CardHeader from '@mui/material/CardHeader'
-import CardContent from '@mui/material/CardContent'
-import Typography from '@mui/material/Typography'
-
-// Third-party Imports
-import type { ApexOptions } from 'apexcharts'
-
-// Component Imports
-import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard'
-import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
-
-// Styled Component Imports
-const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
-
-const DEFAULT_WEATHER = {
- temperature: 24,
- condition: 'آفتابی',
- humidity: 45,
- windSpeed: 12,
- windUnit: 'km/h',
- unit: '°C',
- precipitation: 0
-}
-
-const DEFAULT_FORECAST_SERIES = [
- { name: 'دما', data: [18, 22, 26, 28, 25, 20, 18] },
- { name: 'رطوبت', data: [55, 48, 42, 38, 45, 52, 58] }
-]
-
-const FORECAST_CATEGORIES = ['امروز', 'فردا', 'شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه']
-
-export default function CropZoningWeatherSection() {
- const t = useTranslations('cropZoning.weather')
- const { farmHub } = useFarmHub()
- const farmUuid = farmHub?.farm_uuid
- const [weatherData, setWeatherData] = useState>(DEFAULT_WEATHER)
- const [forecastSeries, setForecastSeries] = useState(DEFAULT_FORECAST_SERIES)
-
- useEffect(() => {
- if (!farmUuid) {
- setWeatherData(DEFAULT_WEATHER)
- setForecastSeries(DEFAULT_FORECAST_SERIES)
- return
- }
-
- farmDashboardService
- .getAllCards(farmUuid)
- .then(cards => {
- const w = cards?.farmWeatherCard
- if (w && typeof w === 'object') {
- setWeatherData({ ...DEFAULT_WEATHER, ...w })
- const chartData = w.chartData as { labels?: string[]; series?: number[][] } | undefined
- if (chartData?.series?.[0]) {
- setForecastSeries([{ name: 'دما', data: chartData.series[0] }])
- }
- }
- })
- .catch(() => {})
- }, [farmUuid])
-
- const forecastOptions: ApexOptions = {
- chart: {
- parentHeightOffset: 0,
- toolbar: { show: false },
- zoom: { enabled: false }
- },
- colors: ['var(--mui-palette-info-main)', 'var(--mui-palette-success-main)'],
- stroke: { width: 2, curve: 'smooth' },
- legend: {
- position: 'top',
- labels: { colors: 'var(--mui-palette-text-secondary)' }
- },
- dataLabels: { enabled: false },
- grid: {
- borderColor: 'var(--mui-palette-divider)',
- strokeDashArray: 4,
- xaxis: { lines: { show: false } },
- yaxis: { lines: { show: true } }
- },
- xaxis: {
- categories: FORECAST_CATEGORIES,
- labels: { style: { colors: 'var(--mui-palette-text-disabled)' } },
- axisBorder: { show: false },
- axisTicks: { show: false }
- },
- yaxis: {
- labels: {
- style: { colors: 'var(--mui-palette-text-disabled)' }
- }
- }
- }
-
- const temp = (weatherData.temperature as number) ?? 24
- const humidity = (weatherData.humidity as number) ?? 45
- const windSpeed = (weatherData.windSpeed as number) ?? 12
-
- const cardRowSx = {
- display: 'flex',
- flexDirection: 'column' as const,
- minHeight: 200,
- '& > *': { flex: 1, minHeight: 0 }
- }
-
- return (
-
-
- {t('title')}
-
-
- {/* Row 1: Weather + 3 KPI cards — equal width (3 each on md+) */}
-
-
-
-
-
-
-
-
-
- {t('temperature')}
-
-
- {temp}
- {(weatherData.unit as string) ?? '°C'}
-
-
-
-
-
-
-
-
-
- {t('humidity')}
-
- {humidity}%
-
-
-
-
-
-
-
-
- {t('windSpeed')}
-
-
- {windSpeed} {(weatherData.windUnit as string) ?? 'km/h'}
-
-
-
-
-
-
- {/* Row 2: Forecast chart — full width */}
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
index f4844a8..4015995 100644
--- a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
+++ b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
@@ -6,18 +6,21 @@ import { useTranslations } from "next-intl";
import { useFarmHub } from "@/hooks/useFarmHub";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
+import Grid from "@mui/material/Grid2";
import LinearProgress from "@mui/material/LinearProgress";
+import NDVIHealthCard from "@views/dashboards/farm/NDVIHealthCard";
+import SatelliteImageDownloadCard from "./SatelliteImageDownloadCard";
import CropZoningMap from "./CropZoningMap";
import ZoneLegend from "./ZoneLegend";
import LayerControl from "./LayerControl";
import ZoneDetailPanel from "./ZoneDetailPanel";
-import CropZoningWeatherSection from "./CropZoningWeatherSection";
import {
cropZoningService,
type Product,
type ZoneInitialData,
type ZoneDetailData,
} from "@/libs/api/services/cropZoningService";
+import { cropHealthService } from "@/libs/api/services/cropHealthService";
import { CROP_COLORS, type CropType } from "./cropZoningTypes";
import type { LayerType } from "./cropZoningTypes";
import type { MapDrawGeoJSON } from "./CropZoningMap";
@@ -55,6 +58,7 @@ export default function CropZoningWrapper() {
const [activeLayer, setActiveLayer] = useState("crops");
const [selectedZone, setSelectedZone] = useState(null);
const [panelOpen, setPanelOpen] = useState(false);
+ const [ndviData, setNdviData] = useState>({});
useEffect(() => {
setIsClientReady(true);
@@ -66,6 +70,18 @@ export default function CropZoningWrapper() {
.catch(() => setProducts([]));
}, []);
+ useEffect(() => {
+ if (!farmUuid) {
+ setNdviData({});
+ return;
+ }
+
+ cropHealthService
+ .getSummary(farmUuid)
+ .then(summary => setNdviData((summary.ndviHealthCard as Record) ?? {}))
+ .catch(() => setNdviData({}));
+ }, [farmUuid]);
+
useEffect(() => {
let cancelled = false;
@@ -362,8 +378,20 @@ export default function CropZoningWrapper() {
+
+
+ *": { height: "100%" } }}>
+
+
+
+
+
+
+
+
+
+
setPanelOpen(false)} zone={selectedZone} products={products} loading={false} />
-
);
}
diff --git a/src/views/dashboards/farm/cropZoning/SatelliteImageDownloadCard.tsx b/src/views/dashboards/farm/cropZoning/SatelliteImageDownloadCard.tsx
new file mode 100644
index 0000000..d3f4405
--- /dev/null
+++ b/src/views/dashboards/farm/cropZoning/SatelliteImageDownloadCard.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import CardHeader from "@mui/material/CardHeader";
+import Chip from "@mui/material/Chip";
+import Stack from "@mui/material/Stack";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+
+interface SatelliteImageDownloadCardProps {
+ farmUuid?: string | null;
+}
+
+const toInputDate = (date: Date) => {
+ const offset = date.getTimezoneOffset();
+ const localDate = new Date(date.getTime() - offset * 60_000);
+
+ return localDate.toISOString().slice(0, 10);
+};
+
+const formatPersianDate = (value: string) => {
+ const date = new Date(`${value}T00:00:00`);
+
+ if (Number.isNaN(date.getTime())) {
+ return "-";
+ }
+
+ return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ }).format(date);
+};
+
+const createSatellitePlaceholderSvg = (farmUuid: string, selectedDate: string) => `
+`;
+
+export default function SatelliteImageDownloadCard({
+ farmUuid,
+}: SatelliteImageDownloadCardProps) {
+ const [selectedDate, setSelectedDate] = useState(toInputDate(new Date()));
+
+ const formattedDate = useMemo(
+ () => formatPersianDate(selectedDate),
+ [selectedDate],
+ );
+
+ const handleDownload = () => {
+ const safeFarmUuid = farmUuid || "no-farm-selected";
+ const svg = createSatellitePlaceholderSvg(safeFarmUuid, selectedDate);
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+
+ link.href = url;
+ link.download = `satellite-${safeFarmUuid}-${selectedDate}.svg`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ }
+ />
+
+
+
+
+
+ Snapshot Preview
+
+ تصویر ثبت شده برای مزرعه انتخابی
+
+ تاریخ انتخابی به صورت یک فایل تصویری دانلود می شود تا تیم مزرعه بتواند
+ وضعیت زمین را برای همان روز آرشیو یا بررسی کند.
+
+
+
+
+
+ `1px solid ${theme.palette.divider}`,
+ backgroundColor: "background.default",
+ }}
+ >
+
+
+ تنظیمات درخواست تصویر
+
+
+
+ setSelectedDate(event.target.value)}
+ InputLabelProps={{ shrink: true }}
+ fullWidth
+ />
+ }
+ sx={{ minWidth: { xs: "100%", md: 220 }, py: 1.7 }}
+ >
+ دانلود عکس ماهواره ای
+
+
+
+
+
+
+
+
+
+
+
+ فعلا خروجی به صورت فایل تصویری placeholder دانلود می شود؛ بعدا می توان این دکمه را
+ به API واقعی تصویر ماهواره ای وصل کرد.
+
+
+
+ );
+}
diff --git a/src/views/dashboards/farm/cropZoning/index.ts b/src/views/dashboards/farm/cropZoning/index.ts
index e3eca74..52dd641 100644
--- a/src/views/dashboards/farm/cropZoning/index.ts
+++ b/src/views/dashboards/farm/cropZoning/index.ts
@@ -1,5 +1,4 @@
export { default as CropZoningWrapper } from './CropZoningWrapper'
-export { default as CropZoningWeatherSection } from './CropZoningWeatherSection'
export { default as CropZoningMap } from './CropZoningMap'
export { default as ZoneLegend } from './ZoneLegend'
export { default as LayerControl } from './LayerControl'