From 8f74e4f3854e70213f6d5e447e3a75ceaf8e439a Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Wed, 29 Apr 2026 03:47:34 +0330 Subject: [PATCH] UPDATE --- messages/fa.json | 1 + .../(private)/crop-health/page.tsx | 7 - .../(private)/economic-overview/page.tsx | 7 - .../(private)/farmer-calendar/page.tsx | 7 + .../(private)/yield-harvest/UI_DESCRIPTION.md | 409 +++++++++++++++ .../layout/vertical/VerticalMenu.tsx | 7 +- src/constants/navigation.ts | 8 +- src/data/dictionaries/ar.json | 12 +- src/data/dictionaries/en.json | 12 +- src/data/dictionaries/fa.json | 12 +- src/data/dictionaries/fr.json | 12 +- src/data/navigation/horizontalMenuData.tsx | 22 +- src/data/navigation/verticalMenuData.tsx | 11 +- src/libs/api/services/farmAlertsService.ts | 87 +++- src/libs/styles/AppFullCalendar.ts | 1 + src/views/apps/calendar/Calendar.tsx | 44 +- .../dashboards/farm/AlertsPageWrapper.tsx | 122 ++++- .../dashboards/farm/CropHealthPageWrapper.tsx | 65 --- .../farm/EconomicOverviewPageWrapper.tsx | 62 --- .../dashboards/farm/FarmerCalendarPage.tsx | 486 ++++++++++++++++++ .../farm/NotificationSettingsCard.tsx | 172 +++++++ .../cropZoning/CropZoningWeatherSection.tsx | 192 ------- .../farm/cropZoning/CropZoningWrapper.tsx | 32 +- .../cropZoning/SatelliteImageDownloadCard.tsx | 220 ++++++++ src/views/dashboards/farm/cropZoning/index.ts | 1 - 25 files changed, 1615 insertions(+), 396 deletions(-) delete mode 100644 src/app/(dashboard)/(private)/crop-health/page.tsx delete mode 100644 src/app/(dashboard)/(private)/economic-overview/page.tsx create mode 100644 src/app/(dashboard)/(private)/farmer-calendar/page.tsx create mode 100644 src/app/(dashboard)/(private)/yield-harvest/UI_DESCRIPTION.md delete mode 100644 src/views/dashboards/farm/CropHealthPageWrapper.tsx delete mode 100644 src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx create mode 100644 src/views/dashboards/farm/FarmerCalendarPage.tsx create mode 100644 src/views/dashboards/farm/NotificationSettingsCard.tsx delete mode 100644 src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx create mode 100644 src/views/dashboards/farm/cropZoning/SatelliteImageDownloadCard.tsx 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) => ` + + + + + + + + + + + + + + + + + + + Satellite Snapshot Request + Farm UUID: ${farmUuid} + Capture Date: ${selectedDate} + This placeholder download can be replaced with a real satellite API image endpoint later. +`; + +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 + /> + + + + + + + + + + + + فعلا خروجی به صورت فایل تصویری 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'