This commit is contained in:
2026-04-29 03:47:34 +03:30
parent 5c548bc6db
commit 8f74e4f385
25 changed files with 1615 additions and 396 deletions
+1
View File
@@ -152,6 +152,7 @@
"farmAlerts": "هشدارهای مزرعه", "farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری", "pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی", "economicOverview": "نمای اقتصادی",
"farmCalendar": "تقویم کشاورز",
"sensorSection": "سنسورها", "sensorSection": "سنسورها",
"sensor7In1": "سنسور خاک 7 در 1" "sensor7In1": "سنسور خاک 7 در 1"
}, },
@@ -1,7 +0,0 @@
import CropHealthPageWrapper from '@views/dashboards/farm/CropHealthPageWrapper'
const CropHealthPage = async () => {
return <CropHealthPageWrapper />
}
export default CropHealthPage
@@ -1,7 +0,0 @@
import EconomicOverviewPageWrapper from '@views/dashboards/farm/EconomicOverviewPageWrapper'
const EconomicOverviewPage = async () => {
return <EconomicOverviewPageWrapper />
}
export default EconomicOverviewPage
@@ -0,0 +1,7 @@
import FarmerCalendarPage from '@views/dashboards/farm/FarmerCalendarPage'
const FarmerCalendar = async () => {
return <FarmerCalendarPage />
}
export default FarmerCalendar
@@ -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.
@@ -95,9 +95,6 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
{t('dashboards')} {t('dashboards')}
</MenuItem> </MenuItem>
<MenuSection label={t('farmDomain')}> <MenuSection label={t('farmDomain')}>
<MenuItem href="/crop-health" icon={<i className="tabler-plant" />}>
{t('cropHealth')}
</MenuItem>
<MenuItem href="/yield-harvest" icon={<i className="tabler-chart-line" />}> <MenuItem href="/yield-harvest" icon={<i className="tabler-chart-line" />}>
{t('yieldHarvest')} {t('yieldHarvest')}
</MenuItem> </MenuItem>
@@ -107,8 +104,8 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
<MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}> <MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}>
{t('pestDiseaseRisk')} {t('pestDiseaseRisk')}
</MenuItem> </MenuItem>
<MenuItem href="/economic-overview" icon={<i className="tabler-currency-dollar" />}> <MenuItem href="/farmer-calendar" icon={<i className="tabler-calendar-event" />}>
{t('economicOverview')} {t('farmCalendar')}
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
<MenuSection label={t('dataSection')}> <MenuSection label={t('dataSection')}>
+7 -1
View File
@@ -4,9 +4,16 @@ export const navigationLabels = {
farm: 'داشبورد مزرعه', farm: 'داشبورد مزرعه',
waterData: 'دیتاهای آب', waterData: 'دیتاهای آب',
soilData: 'اطلاعات خاک', soilData: 'اطلاعات خاک',
cropZoning: 'زون‌بندی کشت',
sensorSection: 'سنسورها', sensorSection: 'سنسورها',
sensor7In1: 'سنسور خاک 7 در 1', sensor7In1: 'سنسور خاک 7 در 1',
dataSection: 'بخش داده‌ها', dataSection: 'بخش داده‌ها',
recommendation: 'توصیه‌ها',
irrigationRecommendation: 'توصیه آبیاری',
fertilizationRecommendation: 'توصیه کوددهی',
aiAssistant: 'دستیار هوشمند',
farmAiAssistant: 'دستیار هوشمند مزرعه',
farmCalendar: 'تقویم کشاورز',
crm: 'مدیریت ارتباط با مشتری', crm: 'مدیریت ارتباط با مشتری',
analytics: 'تحلیل‌ها', analytics: 'تحلیل‌ها',
eCommerce: 'فروشگاه', eCommerce: 'فروشگاه',
@@ -109,4 +116,3 @@ export const navigationLabels = {
menuLevel3: 'سطح منو 3', menuLevel3: 'سطح منو 3',
disabledMenu: 'منوی غیرفعال' disabledMenu: 'منوی غیرفعال'
} }
+11 -1
View File
@@ -110,8 +110,18 @@
"farmAlerts": "تنبيهات المزرعة", "farmAlerts": "تنبيهات المزرعة",
"pestDiseaseRisk": "مخاطر الآفات والأمراض", "pestDiseaseRisk": "مخاطر الآفات والأمراض",
"economicOverview": "النظرة الاقتصادية", "economicOverview": "النظرة الاقتصادية",
"farmCalendar": "تقويم المزارع",
"dataSection": "قسم البيانات",
"waterData": "بيانات المياه",
"soilData": "بيانات التربة",
"cropZoning": "تقسيم المحاصيل",
"sensorSection": "المستشعرات", "sensorSection": "المستشعرات",
"sensor7In1": "مستشعر التربة 7 في 1" "sensor7In1": "مستشعر التربة 7 في 1",
"recommendation": "التوصيات",
"irrigationRecommendation": "توصية الري",
"fertilizationRecommendation": "توصية التسميد",
"aiAssistant": "المساعد الذكي",
"farmAiAssistant": "مساعد المزرعة الذكي"
}, },
"irrigation": { "irrigation": {
"title": "توصية الري الذكية", "title": "توصية الري الذكية",
+11 -1
View File
@@ -110,8 +110,18 @@
"farmAlerts": "Farm Alerts", "farmAlerts": "Farm Alerts",
"pestDiseaseRisk": "Pest & Disease Risk", "pestDiseaseRisk": "Pest & Disease Risk",
"economicOverview": "Economic Overview", "economicOverview": "Economic Overview",
"farmCalendar": "Farmer Calendar",
"dataSection": "Data Section",
"waterData": "Water Data",
"soilData": "Soil Data",
"cropZoning": "Crop Zoning",
"sensorSection": "Sensors", "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": { "irrigation": {
"title": "Smart Irrigation Recommendation", "title": "Smart Irrigation Recommendation",
+11 -1
View File
@@ -110,8 +110,18 @@
"farmAlerts": "هشدارهای مزرعه", "farmAlerts": "هشدارهای مزرعه",
"pestDiseaseRisk": "ریسک آفات و بیماری", "pestDiseaseRisk": "ریسک آفات و بیماری",
"economicOverview": "نمای اقتصادی", "economicOverview": "نمای اقتصادی",
"farmCalendar": "تقویم کشاورز",
"dataSection": "بخش داده‌ها",
"waterData": "دیتاهای آب",
"soilData": "اطلاعات خاک",
"cropZoning": "زون‌بندی کشت",
"sensorSection": "سنسورها", "sensorSection": "سنسورها",
"sensor7In1": "سنسور خاک 7 در 1" "sensor7In1": "سنسور خاک 7 در 1",
"recommendation": "توصیه‌ها",
"irrigationRecommendation": "توصیه آبیاری",
"fertilizationRecommendation": "توصیه کوددهی",
"aiAssistant": "دستیار هوشمند",
"farmAiAssistant": "دستیار هوشمند مزرعه"
}, },
"irrigation": { "irrigation": {
"title": "توصیه آبیاری هوشمند", "title": "توصیه آبیاری هوشمند",
+11 -1
View File
@@ -110,8 +110,18 @@
"farmAlerts": "Alertes ferme", "farmAlerts": "Alertes ferme",
"pestDiseaseRisk": "Risque ravageurs et maladies", "pestDiseaseRisk": "Risque ravageurs et maladies",
"economicOverview": "Aperçu économique", "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", "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": { "irrigation": {
"title": "Recommandation intelligente d'irrigation", "title": "Recommandation intelligente d'irrigation",
+6 -16
View File
@@ -45,11 +45,6 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
icon: 'tabler-dashboard', icon: 'tabler-dashboard',
href: '/dashboard' href: '/dashboard'
}, },
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{ {
label: 'waterWeather', label: 'waterWeather',
icon: 'tabler-droplet', icon: 'tabler-droplet',
@@ -76,9 +71,9 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
href: '/pest-risk' href: '/pest-risk'
}, },
{ {
label: 'economicOverview', label: 'farmCalendar',
icon: 'tabler-currency-dollar', icon: 'tabler-calendar-event',
href: '/economic-overview' href: '/farmer-calendar'
}, },
{ {
label: 'cropZoning', label: 'cropZoning',
@@ -101,11 +96,6 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
icon: 'tabler-dashboard', icon: 'tabler-dashboard',
href: '/dashboard' href: '/dashboard'
}, },
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{ {
label: 'waterWeather', label: 'waterWeather',
icon: 'tabler-droplet', icon: 'tabler-droplet',
@@ -132,9 +122,9 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
href: '/pest-risk' href: '/pest-risk'
}, },
{ {
label: 'economicOverview', label: 'farmCalendar',
icon: 'tabler-currency-dollar', icon: 'tabler-calendar-event',
href: '/economic-overview' href: '/farmer-calendar'
}, },
{ {
label: 'cropZoning', label: 'cropZoning',
+3 -8
View File
@@ -49,11 +49,6 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
icon: 'tabler-dashboard', icon: 'tabler-dashboard',
href: '/dashboard' href: '/dashboard'
}, },
{
label: 'cropHealth',
icon: 'tabler-plant',
href: '/crop-health'
},
{ {
label: 'waterWeather', label: 'waterWeather',
icon: 'tabler-droplet', icon: 'tabler-droplet',
@@ -80,9 +75,9 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
href: '/pest-risk' href: '/pest-risk'
}, },
{ {
label: 'economicOverview', label: 'farmCalendar',
icon: 'tabler-currency-dollar', icon: 'tabler-calendar-event',
href: '/economic-overview' href: '/farmer-calendar'
}, },
{ {
label: 'cropZoning', label: 'cropZoning',
+80 -7
View File
@@ -2,10 +2,73 @@ import { apiClient } from '../client'
const PREFIX = '/api/farm-alerts' const PREFIX = '/api/farm-alerts'
export interface FarmAlertsSummary { export interface FarmAlertRequestItem {
tracker?: Record<string, unknown> alert_id: string
timeline?: Record<string, unknown> level: string
recommendations?: Record<string, unknown> title: string
message: string
suggested_action?: string
source_metric_type?: string
timestamp?: string | null
payload?: Record<string, unknown>
}
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<string, unknown>
is_read: boolean
metadata?: Record<string, unknown>
created_at: string
updated_at?: string
}
export interface FarmAlertsTrackerPayload {
totalAlerts?: number
alerts?: FarmAlertTrackerItem[]
alertStats?: Array<Record<string, unknown>>
alertClusters?: Array<Record<string, unknown>>
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<string, unknown>
} }
interface ApiResponse<T> { interface ApiResponse<T> {
@@ -25,9 +88,19 @@ function extract<T>(res: ApiResponse<T> | T): T {
} }
export const farmAlertsService = { export const farmAlertsService = {
async getTracker(farmUuid: string): Promise<Record<string, unknown>> { async analyzeTracker(
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>( payload: { farmUuid: string; alerts?: FarmAlertRequestItem[] },
`${PREFIX}/tracker/?farm_uuid=${encodeURIComponent(farmUuid)}` ): Promise<FarmAlertsTrackerResponse> {
const requestBody = {
farm_uuid: payload.farmUuid,
...(payload.alerts?.length ? { alerts: payload.alerts } : {}),
}
const res = await apiClient.post<
ApiResponse<FarmAlertsTrackerResponse> | FarmAlertsTrackerResponse
>(
`${PREFIX}/tracker/`,
requestBody,
) )
return extract(res) return extract(res)
}, },
+1
View File
@@ -10,6 +10,7 @@ const AppFullCalendar = styled('div')(({ theme }: { theme: Theme }) => ({
position: 'relative', position: 'relative',
borderRadius: 'var(--mui-shape-borderRadius)', borderRadius: 'var(--mui-shape-borderRadius)',
'& .fc': { '& .fc': {
width: '100%',
zIndex: 1, zIndex: 1,
'.fc-col-header, .fc-daygrid-body, .fc-scrollgrid-sync-table, .fc-timegrid-body, .fc-timegrid-body table': { '.fc-col-header, .fc-daygrid-body, .fc-scrollgrid-sync-table, .fc-timegrid-body, .fc-timegrid-body table': {
+33 -7
View File
@@ -27,9 +27,13 @@ type CalenderProps = {
calendarApi: any calendarApi: any
setCalendarApi: (val: any) => void setCalendarApi: (val: any) => void
calendarsColor: CalendarColors calendarsColor: CalendarColors
dispatch: AppDispatch dispatch?: AppDispatch
handleLeftSidebarToggle: () => void handleLeftSidebarToggle?: () => void
handleAddEventSidebarToggle: () => void handleAddEventSidebarToggle?: () => void
editable?: boolean
showSidebarToggle?: boolean
onDateClick?: (date: Date) => void
onEventClick?: (event: any) => void
} }
const blankEvent: AddEventType = { const blankEvent: AddEventType = {
@@ -54,7 +58,11 @@ const Calendar = (props: CalenderProps) => {
calendarsColor, calendarsColor,
dispatch, dispatch,
handleAddEventSidebarToggle, handleAddEventSidebarToggle,
handleLeftSidebarToggle handleLeftSidebarToggle,
editable = true,
showSidebarToggle = true,
onDateClick,
onEventClick
} = props } = props
// Refs // Refs
@@ -78,7 +86,7 @@ const Calendar = (props: CalenderProps) => {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
locale: 'fa', locale: 'fa',
headerToolbar: { headerToolbar: {
start: 'sidebarToggle,prev,next,title', start: `${showSidebarToggle ? 'sidebarToggle,' : ''}prev,next,title`,
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth' end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
}, },
views: { views: {
@@ -153,7 +161,7 @@ const Calendar = (props: CalenderProps) => {
Enable dragging and resizing event Enable dragging and resizing event
? Docs: https://fullcalendar.io/docs/editable ? Docs: https://fullcalendar.io/docs/editable
*/ */
editable: true, editable,
/* /*
Enable resizing event from start Enable resizing event from start
@@ -192,8 +200,12 @@ const Calendar = (props: CalenderProps) => {
eventClick({ event: clickedEvent, jsEvent }: any) { eventClick({ event: clickedEvent, jsEvent }: any) {
jsEvent.preventDefault() jsEvent.preventDefault()
onEventClick?.(clickedEvent)
if (dispatch && handleAddEventSidebarToggle) {
dispatch(selectedEvent(clickedEvent)) dispatch(selectedEvent(clickedEvent))
handleAddEventSidebarToggle() handleAddEventSidebarToggle()
}
if (clickedEvent.url) { if (clickedEvent.url) {
// Open the URL in a new tab // Open the URL in a new tab
@@ -210,12 +222,18 @@ const Calendar = (props: CalenderProps) => {
sidebarToggle: { sidebarToggle: {
icon: 'tabler tabler-menu-2', icon: 'tabler tabler-menu-2',
click() { click() {
handleLeftSidebarToggle() handleLeftSidebarToggle?.()
} }
} }
}, },
dateClick(info: any) { dateClick(info: any) {
onDateClick?.(info.date)
if (!dispatch || !handleAddEventSidebarToggle) {
return
}
const ev = { ...blankEvent } const ev = { ...blankEvent }
ev.start = info.date 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 ? 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) { eventDrop({ event: droppedEvent }: any) {
if (!dispatch) {
return
}
// Convert FullCalendar event to API format // Convert FullCalendar event to API format
const eventData = { const eventData = {
start: droppedEvent.start ? new Date(droppedEvent.start).toISOString() : '', start: droppedEvent.start ? new Date(droppedEvent.start).toISOString() : '',
@@ -247,6 +269,10 @@ const Calendar = (props: CalenderProps) => {
? Docs: https://fullcalendar.io/docs/eventResize ? Docs: https://fullcalendar.io/docs/eventResize
*/ */
eventResize({ event: resizedEvent }: any) { eventResize({ event: resizedEvent }: any) {
if (!dispatch) {
return
}
// Convert FullCalendar event to API format // Convert FullCalendar event to API format
const eventData = { const eventData = {
start: resizedEvent.start ? new Date(resizedEvent.start).toISOString() : '', start: resizedEvent.start ? new Date(resizedEvent.start).toISOString() : '',
+112 -10
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub' import { useFarmHub } from '@/hooks/useFarmHub'
import { format } from 'date-fns'
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
@@ -9,9 +10,15 @@ import CircularProgress from '@mui/material/CircularProgress'
import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker' import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker'
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline' import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
import NotificationSettingsCard from '@views/dashboards/farm/NotificationSettingsCard'
import RecommendationsList from '@views/dashboards/farm/RecommendationsList' 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 = { const cardRowSx = {
display: 'flex', display: 'flex',
@@ -20,6 +27,94 @@ const cardRowSx = {
'& > *': { flex: 1, minHeight: 0 } '& > *': { 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<string, unknown> => {
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<Record<string, unknown>>).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<string, unknown> => {
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<string, unknown> => {
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 AlertsPageWrapper = () => {
const { farmHub } = useFarmHub() const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid const farmUuid = farmHub?.farm_uuid
@@ -38,15 +133,19 @@ const AlertsPageWrapper = () => {
} }
setLoading(true) setLoading(true)
Promise.all([ farmAlertsService
farmAlertsService.getTracker(farmUuid).catch(() => ({})), .analyzeTracker({ farmUuid })
farmAlertsService.getTimeline(farmUuid).catch(() => ({})), .then(result => {
farmAlertsService.getRecommendations(farmUuid).catch(() => ({})) const notifications = Array.isArray(result.notifications) ? result.notifications : []
])
.then(([t, tl, r]) => { setTracker(buildTrackerCardData(result))
setTracker(t) setTimeline(buildTimelineData(result, notifications))
setTimeline(tl) setRecommendations(buildRecommendationsData(result))
setRecommendations(r) })
.catch(() => {
setTracker({})
setTimeline({})
setRecommendations({})
}) })
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, [farmUuid]) }, [farmUuid])
@@ -73,6 +172,9 @@ const AlertsPageWrapper = () => {
<RecommendationsList data={recommendations} /> <RecommendationsList data={recommendations} />
</Grid> </Grid>
</Grid> </Grid>
<Grid size={12} sx={cardRowSx}>
<NotificationSettingsCard />
</Grid>
</Grid> </Grid>
</Box> </Box>
) )
@@ -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<Record<string, unknown>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
cropHealthService
.getSummary(farmUuid)
.then(summary => setData((summary as Record<string, unknown>) ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} container spacing={6}>
<FarmOverviewKPIs data={data.farm_health_score as Record<string, unknown>} />
</Grid>
<Grid size={12} sx={cardRowSx}>
<NDVIHealthCard data={data.ndviHealthCard as Record<string, unknown>} />
</Grid>
</Grid>
</Box>
)
}
export default CropHealthPageWrapper
@@ -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<EconomicOverviewSummary>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!farmUuid) {
setData({})
setLoading(false)
return
}
setLoading(true)
economicOverviewService
.getSummary(farmUuid)
.then(summary => setData(summary ?? {}))
.catch(() => setData({}))
.finally(() => setLoading(false))
}, [farmUuid])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} sx={cardRowSx}>
<EconomicOverview data={data.economicOverview as Record<string, unknown>} />
</Grid>
</Grid>
</Box>
)
}
export default EconomicOverviewPageWrapper
@@ -0,0 +1,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<any>(null)
const [selectedEvent, setSelectedEvent] = useState<EventInput>(upcomingEvents[0])
const selectedEventDate = useMemo(() => fullDateFormatter.format(getEventDate(selectedEvent)), [selectedEvent])
return (
<Box className='flex flex-col gap-6'>
<Card
sx={{
position: 'relative',
overflow: 'hidden',
color: 'common.white',
background:
'linear-gradient(135deg, #123524 0%, #1d5b3f 45%, #3f8f5d 100%)'
}}
>
<Box
sx={{
position: 'absolute',
insetInlineEnd: -40,
insetBlockStart: -60,
width: 220,
height: 220,
borderRadius: '50%',
background: 'rgba(255,255,255,0.08)'
}}
/>
<CardContent sx={{ p: { xs: 5, md: 8 } }}>
<Grid container spacing={4} alignItems='center'>
<Grid size={{ xs: 12, lg: 7 }}>
<Stack spacing={2.5}>
<Chip
label='برنامه ریزی هوشمند مزرعه'
color='success'
variant='filled'
sx={{
alignSelf: 'flex-start',
bgcolor: 'rgba(255,255,255,0.14)',
color: 'common.white',
fontWeight: 700
}}
/>
<Typography variant='h3' sx={{ color: 'inherit', maxWidth: 720 }}>
تقویم بزرگ عملیات کشاورز برای مدیریت آبیاری، تغذیه، برداشت و کارهای روزانه
</Typography>
<Typography variant='body1' sx={{ color: 'rgba(255,255,255,0.82)', maxWidth: 680 }}>
این صفحه تمام برنامه های مزرعه را در یک نمای ماهانه جمع می کند تا کشاورز بداند
امروز چه کاری مهم تر است و در روزهای آینده چه چیزی باید آماده شود.
</Typography>
</Stack>
</Grid>
<Grid size={{ xs: 12, lg: 5 }}>
<Grid container spacing={3}>
{overviewItems.map(item => (
<Grid key={item.label} size={{ xs: 12, md: 4, lg: 12 }}>
<Card
variant='outlined'
sx={{
height: '100%',
borderColor: 'rgba(255,255,255,0.12)',
background: item.accent,
backdropFilter: 'blur(6px)',
color: 'common.white'
}}
>
<CardContent>
<Typography variant='body2' sx={{ color: 'rgba(255,255,255,0.74)' }}>
{item.label}
</Typography>
<Typography variant='h5' sx={{ color: 'inherit', my: 1.5 }}>
{item.value}
</Typography>
<Typography variant='body2' sx={{ color: 'rgba(255,255,255,0.74)' }}>
{item.note}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Grid>
</Grid>
</CardContent>
</Card>
<Grid container spacing={6}>
<Grid size={{ xs: 12, xl: 8.5 }}>
<Card className='overflow-visible'>
<CardContent sx={{ p: { xs: 4, md: 6 } }}>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={2}
alignItems={{ xs: 'flex-start', md: 'center' }}
justifyContent='space-between'
sx={{ mb: 4 }}
>
<Box>
<Typography variant='h5'>تقویم عملیات مزرعه</Typography>
<Typography variant='body2' color='text.secondary'>
روی هر برنامه کلیک کن تا جزئیات آن در پنل کناری نمایش داده شود.
</Typography>
</Box>
<Stack direction='row' spacing={1.5} flexWrap='wrap' useFlexGap>
{legendItems.map(item => (
<Stack key={item.label} direction='row' spacing={1} alignItems='center'>
<Box
sx={{
width: 10,
height: 10,
borderRadius: '50%',
bgcolor: item.color
}}
/>
<Typography variant='caption' color='text.secondary'>
{item.label}
</Typography>
</Stack>
))}
</Stack>
</Stack>
<AppFullCalendar
className='app-calendar'
sx={{
'& .fc': {
'& .fc-view-harness': {
minHeight: { xs: 620, md: 760 }
},
'& .fc-toolbar-title': {
fontSize: { xs: '1.2rem', md: '1.8rem' }
}
}
}}
>
<Calendar
calendarStore={calendarStore}
calendarApi={calendarApi}
setCalendarApi={setCalendarApi}
calendarsColor={calendarColors}
editable={false}
showSidebarToggle={false}
onEventClick={event => 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)
}}
/>
</AppFullCalendar>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, xl: 3.5 }}>
<Stack spacing={4}>
<Card>
<CardContent sx={{ p: 5 }}>
<Stack spacing={2.5}>
<Box>
<Typography variant='overline' color='success.main'>
برنامه انتخاب شده
</Typography>
<Typography variant='h5' sx={{ mt: 1 }}>
{selectedEvent.title}
</Typography>
</Box>
<Chip
label={getCalendarLabel(selectedEvent)}
color={getCalendarChipColor(selectedEvent)}
variant='tonal'
sx={{ alignSelf: 'flex-start', fontWeight: 700 }}
/>
<Box>
<Typography variant='body2' color='text.secondary'>
تاریخ
</Typography>
<Typography variant='body1' sx={{ mt: 0.5 }}>
{selectedEventDate}
</Typography>
</Box>
<Box>
<Typography variant='body2' color='text.secondary'>
بازه زمانی
</Typography>
<Typography variant='body1' sx={{ mt: 0.5 }}>
{formatTimeRange(selectedEvent)}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant='body2' color='text.secondary'>
توضیحات اجرایی
</Typography>
<Typography variant='body1' sx={{ mt: 1.5, lineHeight: 1.9 }}>
{selectedEvent.extendedProps?.description as string}
</Typography>
</Box>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent sx={{ p: 5 }}>
<Typography variant='h6' sx={{ mb: 3 }}>
برنامه های نزدیک
</Typography>
<Stack spacing={2.5}>
{upcomingEvents.map(event => (
<Box
key={event.id}
onClick={() => 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)'
}
}}
>
<Stack direction='row' justifyContent='space-between' spacing={2}>
<Box>
<Typography variant='subtitle2'>{event.title}</Typography>
<Typography variant='caption' color='text.secondary'>
{dayFormatter.format(getEventDate(event))}
</Typography>
</Box>
<Chip
size='small'
label={getCalendarLabel(event)}
color={getCalendarChipColor(event)}
variant='tonal'
/>
</Stack>
</Box>
))}
</Stack>
</CardContent>
</Card>
</Stack>
</Grid>
</Grid>
</Box>
)
}
export default FarmerCalendarPage
@@ -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<NotificationChannelSetting[]>(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 (
<Card
sx={{
height: '100%',
overflow: 'hidden',
background:
'linear-gradient(180deg, rgba(14,165,233,0.05) 0%, rgba(255,255,255,1) 42%)'
}}
>
<CardHeader
title={title}
subheader={subtitle}
action={
<Chip
size='small'
color='success'
variant='tonal'
label={`${enabledCount} کانال فعال`}
sx={{ fontWeight: 700 }}
/>
}
/>
<CardContent sx={{ pt: 0 }}>
<Stack spacing={2.5}>
{settings.map((item, index) => (
<Box key={item.key}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent='space-between'
sx={{
p: 3,
borderRadius: 3,
border: theme => `1px solid ${theme.palette.divider}`,
backgroundColor: item.enabled ? 'action.hover' : 'background.paper'
}}
>
<Stack direction='row' spacing={2} alignItems='flex-start'>
<Box
sx={{
display: 'grid',
placeItems: 'center',
width: 42,
height: 42,
borderRadius: 2,
color: item.enabled ? 'primary.main' : 'text.disabled',
backgroundColor: item.enabled ? 'primary.lightOpacity' : 'action.selected'
}}
>
<i className={item.icon} />
</Box>
<Box>
<Typography variant='subtitle1'>{item.label}</Typography>
<Typography variant='body2' color='text.secondary' sx={{ mt: 0.5 }}>
{item.description}
</Typography>
</Box>
</Stack>
<FormControlLabel
sx={{ m: 0 }}
control={
<Switch
checked={item.enabled}
color='success'
onChange={() => handleToggle(item.key)}
/>
}
label={
<Typography variant='body2' color={item.enabled ? 'success.main' : 'text.secondary'}>
{item.enabled ? 'فعال' : 'غیرفعال'}
</Typography>
}
labelPlacement='start'
/>
</Stack>
{index < settings.length - 1 ? <Divider sx={{ my: 0.5, opacity: 0 }} /> : null}
</Box>
))}
</Stack>
</CardContent>
</Card>
)
}
export default NotificationSettingsCard
@@ -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<Record<string, unknown>>(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 (
<Box className='pis-0 pie-0'>
<Typography variant='h6' className='mbe-4 font-semibold'>
{t('title')}
</Typography>
<Grid container spacing={3}>
{/* Row 1: Weather + 3 KPI cards — equal width (3 each on md+) */}
<Grid
size={12}
container
spacing={3}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
<Grid size={{ xs: 12, md: 3 }} sx={cardRowSx}>
<FarmWeatherCard data={weatherData} />
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-temperature text-3xl text-error' />
<Typography variant='body2' color='text.secondary'>
{t('temperature')}
</Typography>
<Typography variant='h5'>
{temp}
{(weatherData.unit as string) ?? '°C'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-droplet text-3xl text-info' />
<Typography variant='body2' color='text.secondary'>
{t('humidity')}
</Typography>
<Typography variant='h5'>{humidity}%</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-wind text-3xl text-success' />
<Typography variant='body2' color='text.secondary'>
{t('windSpeed')}
</Typography>
<Typography variant='h5'>
{windSpeed} {(weatherData.windUnit as string) ?? 'km/h'}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Row 2: Forecast chart — full width */}
<Grid size={12} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 280 }}>
<CardHeader
title={t('forecastChart')}
subheader={t('forecastSubheader')}
/>
<CardContent sx={{ flex: 1, minHeight: 0 }}>
<AppReactApexCharts
type='line'
height={260}
width='100%'
series={forecastSeries}
options={forecastOptions}
/>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
)
}
@@ -6,18 +6,21 @@ import { useTranslations } from "next-intl";
import { useFarmHub } from "@/hooks/useFarmHub"; import { useFarmHub } from "@/hooks/useFarmHub";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Grid from "@mui/material/Grid2";
import LinearProgress from "@mui/material/LinearProgress"; import LinearProgress from "@mui/material/LinearProgress";
import NDVIHealthCard from "@views/dashboards/farm/NDVIHealthCard";
import SatelliteImageDownloadCard from "./SatelliteImageDownloadCard";
import CropZoningMap from "./CropZoningMap"; import CropZoningMap from "./CropZoningMap";
import ZoneLegend from "./ZoneLegend"; import ZoneLegend from "./ZoneLegend";
import LayerControl from "./LayerControl"; import LayerControl from "./LayerControl";
import ZoneDetailPanel from "./ZoneDetailPanel"; import ZoneDetailPanel from "./ZoneDetailPanel";
import CropZoningWeatherSection from "./CropZoningWeatherSection";
import { import {
cropZoningService, cropZoningService,
type Product, type Product,
type ZoneInitialData, type ZoneInitialData,
type ZoneDetailData, type ZoneDetailData,
} from "@/libs/api/services/cropZoningService"; } from "@/libs/api/services/cropZoningService";
import { cropHealthService } from "@/libs/api/services/cropHealthService";
import { CROP_COLORS, type CropType } from "./cropZoningTypes"; import { CROP_COLORS, type CropType } from "./cropZoningTypes";
import type { LayerType } from "./cropZoningTypes"; import type { LayerType } from "./cropZoningTypes";
import type { MapDrawGeoJSON } from "./CropZoningMap"; import type { MapDrawGeoJSON } from "./CropZoningMap";
@@ -55,6 +58,7 @@ export default function CropZoningWrapper() {
const [activeLayer, setActiveLayer] = useState<LayerType>("crops"); const [activeLayer, setActiveLayer] = useState<LayerType>("crops");
const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null); const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null);
const [panelOpen, setPanelOpen] = useState(false); const [panelOpen, setPanelOpen] = useState(false);
const [ndviData, setNdviData] = useState<Record<string, unknown>>({});
useEffect(() => { useEffect(() => {
setIsClientReady(true); setIsClientReady(true);
@@ -66,6 +70,18 @@ export default function CropZoningWrapper() {
.catch(() => setProducts([])); .catch(() => setProducts([]));
}, []); }, []);
useEffect(() => {
if (!farmUuid) {
setNdviData({});
return;
}
cropHealthService
.getSummary(farmUuid)
.then(summary => setNdviData((summary.ndviHealthCard as Record<string, unknown>) ?? {}))
.catch(() => setNdviData({}));
}, [farmUuid]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -362,8 +378,20 @@ export default function CropZoningWrapper() {
<ZoneLegend activeLayer={activeLayer} products={products} loading={false} /> <ZoneLegend activeLayer={activeLayer} products={products} loading={false} />
</Box> </Box>
<Grid container spacing={6} alignItems="stretch">
<Grid size={{ xs: 12, xl: 5 }} sx={{ display: "flex" }}>
<Box sx={{ width: "100%", "& > *": { height: "100%" } }}>
<NDVIHealthCard data={ndviData} />
</Box>
</Grid>
<Grid size={{ xs: 12, xl: 7 }} sx={{ display: "flex" }}>
<Box sx={{ width: "100%" }}>
<SatelliteImageDownloadCard farmUuid={farmUuid} />
</Box>
</Grid>
</Grid>
<ZoneDetailPanel open={panelOpen} onClose={() => setPanelOpen(false)} zone={selectedZone} products={products} loading={false} /> <ZoneDetailPanel open={panelOpen} onClose={() => setPanelOpen(false)} zone={selectedZone} products={products} loading={false} />
<CropZoningWeatherSection />
</Box> </Box>
); );
} }
@@ -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) => `<?xml version="1.0" encoding="UTF-8"?>
<svg width="1400" height="900" viewBox="0 0 1400 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1400" y2="900" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B4332"/>
<stop offset="1" stop-color="#588157"/>
</linearGradient>
<pattern id="grid" width="120" height="120" patternUnits="userSpaceOnUse">
<path d="M120 0H0V120" stroke="rgba(255,255,255,0.16)" stroke-width="2"/>
</pattern>
</defs>
<rect width="1400" height="900" fill="url(#bg)"/>
<rect width="1400" height="900" fill="url(#grid)"/>
<rect x="110" y="120" width="1180" height="660" rx="42" fill="rgba(10, 25, 18, 0.38)" stroke="rgba(255,255,255,0.22)" stroke-width="4"/>
<path d="M250 260C346 208 487 214 596 268C698 319 789 344 901 312C1004 281 1104 296 1180 354V664C1079 620 980 618 886 647C763 684 648 665 532 603C415 541 341 521 250 554V260Z" fill="#90BE6D" fill-opacity="0.72"/>
<path d="M282 327C371 285 481 291 571 336C654 378 752 403 842 381C959 352 1045 356 1140 410" stroke="#D9ED92" stroke-width="18" stroke-linecap="round" stroke-dasharray="20 18"/>
<path d="M308 614C420 568 503 573 600 631C705 694 816 709 932 669C1011 642 1084 641 1149 659" stroke="#081C15" stroke-opacity="0.52" stroke-width="26" stroke-linecap="round"/>
<circle cx="1118" cy="232" r="78" fill="rgba(255,255,255,0.08)"/>
<circle cx="1118" cy="232" r="54" fill="rgba(255,255,255,0.12)"/>
<text x="170" y="190" fill="white" font-family="Arial, sans-serif" font-size="46" font-weight="700">Satellite Snapshot Request</text>
<text x="170" y="244" fill="rgba(255,255,255,0.82)" font-family="Arial, sans-serif" font-size="28">Farm UUID: ${farmUuid}</text>
<text x="170" y="286" fill="rgba(255,255,255,0.82)" font-family="Arial, sans-serif" font-size="28">Capture Date: ${selectedDate}</text>
<text x="170" y="755" fill="rgba(255,255,255,0.78)" font-family="Arial, sans-serif" font-size="24">This placeholder download can be replaced with a real satellite API image endpoint later.</text>
</svg>`;
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 (
<Card
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
borderRadius: 4,
}}
>
<CardHeader
title="دانلود تصویر ماهواره ای"
subheader="یک روز مشخص را انتخاب کن تا اسنپ شات زمین برای دانلود آماده شود."
action={<Chip size="small" color="info" variant="tonal" label="Satellite" />}
/>
<CardContent
sx={{
pt: 0,
display: "flex",
flex: 1,
flexDirection: "column",
gap: 3,
}}
>
<Box
sx={{
minHeight: 180,
borderRadius: 4,
p: { xs: 3, md: 4 },
color: "common.white",
position: "relative",
overflow: "hidden",
background:
"linear-gradient(135deg, rgba(17,24,39,1) 0%, rgba(35,84,61,1) 52%, rgba(111,163,91,1) 100%)",
}}
>
<Box
sx={{
position: "absolute",
inset: 0,
opacity: 0.15,
backgroundImage:
"linear-gradient(rgba(255,255,255,0.7) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.7) 1px, transparent 1px)",
backgroundSize: "44px 44px",
}}
/>
<Stack spacing={1.5} sx={{ position: "relative", zIndex: 1 }}>
<Typography variant="overline" sx={{ opacity: 0.82 }}>
Snapshot Preview
</Typography>
<Typography variant="h5">تصویر ثبت شده برای مزرعه انتخابی</Typography>
<Typography variant="body2" sx={{ opacity: 0.86, maxWidth: 480 }}>
تاریخ انتخابی به صورت یک فایل تصویری دانلود می شود تا تیم مزرعه بتواند
وضعیت زمین را برای همان روز آرشیو یا بررسی کند.
</Typography>
<Chip
label={formattedDate}
color="success"
variant="filled"
sx={{ alignSelf: "flex-start", fontWeight: 700 }}
/>
</Stack>
</Box>
<Box
sx={{
p: 3,
borderRadius: 3,
border: theme => `1px solid ${theme.palette.divider}`,
backgroundColor: "background.default",
}}
>
<Stack spacing={2.5}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
تنظیمات درخواست تصویر
</Typography>
<Stack
direction={{ xs: "column", md: "row" }}
spacing={2}
alignItems={{ xs: "stretch", md: "center" }}
>
<TextField
label="تاریخ تصویر"
type="date"
value={selectedDate}
onChange={event => setSelectedDate(event.target.value)}
InputLabelProps={{ shrink: true }}
fullWidth
/>
<Button
variant="contained"
color="success"
size="large"
onClick={handleDownload}
disabled={!farmUuid}
startIcon={<i className="tabler-download" />}
sx={{ minWidth: { xs: "100%", md: 220 }, py: 1.7 }}
>
دانلود عکس ماهواره ای
</Button>
</Stack>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={1.5}
useFlexGap
flexWrap="wrap"
>
<Chip
size="small"
color="success"
variant="tonal"
label={`تاریخ انتخابی: ${formattedDate}`}
/>
<Chip
size="small"
color={farmUuid ? "primary" : "default"}
variant="tonal"
label={farmUuid ? "مزرعه انتخاب شده" : "ابتدا مزرعه را انتخاب کن"}
/>
</Stack>
</Stack>
</Box>
<Typography variant="caption" color="text.secondary">
فعلا خروجی به صورت فایل تصویری placeholder دانلود می شود؛ بعدا می توان این دکمه را
به API واقعی تصویر ماهواره ای وصل کرد.
</Typography>
</CardContent>
</Card>
);
}
@@ -1,5 +1,4 @@
export { default as CropZoningWrapper } from './CropZoningWrapper' export { default as CropZoningWrapper } from './CropZoningWrapper'
export { default as CropZoningWeatherSection } from './CropZoningWeatherSection'
export { default as CropZoningMap } from './CropZoningMap' export { default as CropZoningMap } from './CropZoningMap'
export { default as ZoneLegend } from './ZoneLegend' export { default as ZoneLegend } from './ZoneLegend'
export { default as LayerControl } from './LayerControl' export { default as LayerControl } from './LayerControl'