From ec679c3287a10fbfc45418de141a8360efd76a19 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Thu, 19 Feb 2026 15:48:32 +0330 Subject: [PATCH] Add farm dashboard components including Anomaly Detection, Economic Overview, and Alerts Tracker. Implemented context for navbar slot management and integrated new API service for farm dashboard configuration. Updated navigation menus to include farm dashboard links. --- .../(private)/dashboards/farm/page.tsx | 8 + src/components/Providers.tsx | 3 + .../layout/vertical/NavbarContent.tsx | 9 + src/contexts/navbarSlotContext.tsx | 35 +++ src/data/navigation/horizontalMenuData.tsx | 5 + src/data/navigation/verticalMenuData.tsx | 5 + src/libs/api/index.ts | 1 + src/libs/api/services/farmDashboardService.ts | 118 ++++++++ .../dashboards/farm/AnomalyDetectionCard.tsx | 66 +++++ .../dashboards/farm/EconomicOverview.tsx | 114 ++++++++ .../dashboards/farm/FarmAlertsTimeline.tsx | 104 +++++++ .../dashboards/farm/FarmAlertsTracker.tsx | 128 +++++++++ .../farm/FarmDashboardSettingsDrawer.tsx | 99 +++++++ .../farm/FarmDashboardSettingsDropdown.tsx | 152 +++++++++++ .../dashboards/farm/FarmDashboardWrapper.tsx | 253 ++++++++++++++++++ .../dashboards/farm/FarmOverviewKPIs.tsx | 100 +++++++ src/views/dashboards/farm/FarmWeatherCard.tsx | 98 +++++++ .../dashboards/farm/HarvestPredictionCard.tsx | 40 +++ src/views/dashboards/farm/NDVIHealthCard.tsx | 112 ++++++++ .../dashboards/farm/RecommendationsList.tsx | 79 ++++++ .../dashboards/farm/SensorComparisonChart.tsx | 91 +++++++ .../dashboards/farm/SensorRadarChart.tsx | 85 ++++++ .../dashboards/farm/SensorValuesList.tsx | 72 +++++ .../dashboards/farm/SoilMoistureHeatmap.tsx | 81 ++++++ .../dashboards/farm/WaterNeedPrediction.tsx | 87 ++++++ .../dashboards/farm/YieldPredictionChart.tsx | 104 +++++++ .../dashboards/farm/farmDashboardConfig.ts | 134 ++++++++++ 27 files changed, 2183 insertions(+) create mode 100644 src/app/(dashboard)/(private)/dashboards/farm/page.tsx create mode 100644 src/contexts/navbarSlotContext.tsx create mode 100644 src/libs/api/services/farmDashboardService.ts create mode 100644 src/views/dashboards/farm/AnomalyDetectionCard.tsx create mode 100644 src/views/dashboards/farm/EconomicOverview.tsx create mode 100644 src/views/dashboards/farm/FarmAlertsTimeline.tsx create mode 100644 src/views/dashboards/farm/FarmAlertsTracker.tsx create mode 100644 src/views/dashboards/farm/FarmDashboardSettingsDrawer.tsx create mode 100644 src/views/dashboards/farm/FarmDashboardSettingsDropdown.tsx create mode 100644 src/views/dashboards/farm/FarmDashboardWrapper.tsx create mode 100644 src/views/dashboards/farm/FarmOverviewKPIs.tsx create mode 100644 src/views/dashboards/farm/FarmWeatherCard.tsx create mode 100644 src/views/dashboards/farm/HarvestPredictionCard.tsx create mode 100644 src/views/dashboards/farm/NDVIHealthCard.tsx create mode 100644 src/views/dashboards/farm/RecommendationsList.tsx create mode 100644 src/views/dashboards/farm/SensorComparisonChart.tsx create mode 100644 src/views/dashboards/farm/SensorRadarChart.tsx create mode 100644 src/views/dashboards/farm/SensorValuesList.tsx create mode 100644 src/views/dashboards/farm/SoilMoistureHeatmap.tsx create mode 100644 src/views/dashboards/farm/WaterNeedPrediction.tsx create mode 100644 src/views/dashboards/farm/YieldPredictionChart.tsx create mode 100644 src/views/dashboards/farm/farmDashboardConfig.ts diff --git a/src/app/(dashboard)/(private)/dashboards/farm/page.tsx b/src/app/(dashboard)/(private)/dashboards/farm/page.tsx new file mode 100644 index 0000000..deaf1f6 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboards/farm/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import FarmDashboardWrapper from '@views/dashboards/farm/FarmDashboardWrapper' + +const DashboardFarm = async () => { + return +} + +export default DashboardFarm diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 0ef2ef1..e5ac8bd 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -3,6 +3,7 @@ import type { ChildrenType, Direction } from '@core/types' // Context Imports import { AuthProvider } from '@/contexts/authProvider' +import { NavbarSlotProvider } from '@/contexts/navbarSlotContext' import { VerticalNavProvider } from '@menu/contexts/verticalNavContext' import { SettingsProvider } from '@core/contexts/settingsContext' import ThemeProvider from '@components/theme' @@ -29,6 +30,7 @@ const Providers = async (props: Props) => { return ( + @@ -37,6 +39,7 @@ const Providers = async (props: Props) => { + ) } diff --git a/src/components/layout/vertical/NavbarContent.tsx b/src/components/layout/vertical/NavbarContent.tsx index 3844404..209fc87 100644 --- a/src/components/layout/vertical/NavbarContent.tsx +++ b/src/components/layout/vertical/NavbarContent.tsx @@ -1,5 +1,11 @@ +'use client' + // Third-party Imports import classnames from 'classnames' +import { useContext } from 'react' + +// Context Imports +import NavbarSlotContext from '@/contexts/navbarSlotContext' // Type Imports import type { ShortcutsType } from '@components/layout/shared/ShortcutsDropdown' @@ -106,11 +112,14 @@ const notifications: NotificationsType[] = [ ] const NavbarContent = () => { + const { slotContent } = useContext(NavbarSlotContext) + return (
+ {slotContent}
diff --git a/src/contexts/navbarSlotContext.tsx b/src/contexts/navbarSlotContext.tsx new file mode 100644 index 0000000..7159de5 --- /dev/null +++ b/src/contexts/navbarSlotContext.tsx @@ -0,0 +1,35 @@ +'use client' + +// React Imports +import { createContext, useCallback, useMemo, useState } from 'react' + +// Type Imports +import type { ChildrenType } from '@core/types' +import type { ReactNode } from 'react' + +export type NavbarSlotContextProps = { + slotContent: ReactNode + setSlotContent: (content: ReactNode) => void +} + +const NavbarSlotContext = createContext({} as NavbarSlotContextProps) + +export const NavbarSlotProvider = ({ children }: ChildrenType) => { + const [slotContent, setSlotContentState] = useState(null) + + const setSlotContent = useCallback((content: ReactNode) => { + setSlotContentState(content) + }, []) + + const value = useMemo( + () => ({ + slotContent, + setSlotContent + }), + [slotContent, setSlotContent] + ) + + return {children} +} + +export default NavbarSlotContext diff --git a/src/data/navigation/horizontalMenuData.tsx b/src/data/navigation/horizontalMenuData.tsx index 9c81761..c50a8ab 100644 --- a/src/data/navigation/horizontalMenuData.tsx +++ b/src/data/navigation/horizontalMenuData.tsx @@ -28,6 +28,11 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [ label: 'logistics', icon: 'tabler-truck', href: '/dashboards/logistics' + }, + { + label: 'farm', + icon: 'tabler-plant-2', + href: '/dashboards/farm' } ] }, diff --git a/src/data/navigation/verticalMenuData.tsx b/src/data/navigation/verticalMenuData.tsx index 22f6bb3..64981ff 100644 --- a/src/data/navigation/verticalMenuData.tsx +++ b/src/data/navigation/verticalMenuData.tsx @@ -32,6 +32,11 @@ const verticalMenuData = (): VerticalMenuDataType[] => [ label: 'logistics', icon: 'tabler-circle', href: '/dashboards/logistics' + }, + { + label: 'farm', + icon: 'tabler-plant-2', + href: '/dashboards/farm' } ] }, diff --git a/src/libs/api/index.ts b/src/libs/api/index.ts index c96bba1..47e1364 100644 --- a/src/libs/api/index.ts +++ b/src/libs/api/index.ts @@ -15,4 +15,5 @@ export * from './services/todoService' export * from './services/userManagementService' export * from './services/rolesPermissionsService' export * from './services/sensorHubService' +export * from './services/farmDashboardService' diff --git a/src/libs/api/services/farmDashboardService.ts b/src/libs/api/services/farmDashboardService.ts new file mode 100644 index 0000000..f63b65d --- /dev/null +++ b/src/libs/api/services/farmDashboardService.ts @@ -0,0 +1,118 @@ +/** + * Farm Dashboard Config Service + * Handles API calls for dashboard customization (disabled cards, row order). + * Authenticated user required. + */ + +import { apiClient } from '../client' +import type { FarmDashboardConfig } from '@/views/dashboards/farm/farmDashboardConfig' + +export interface ApiResponse { + code: number + msg: string + data: T +} + +export interface FarmDashboardConfigResponse { + disabled_card_ids: string[] + row_order: string[] + enable_drag_reorder?: boolean +} + +const STORAGE_KEY = 'farm_dashboard_config' + +/** + * Transform API response to frontend config format + */ +function fromApiResponse(data: FarmDashboardConfigResponse): FarmDashboardConfig { + return { + disabledCardIds: data.disabled_card_ids ?? [], + rowOrder: data.row_order ?? [], + enableDragReorder: data.enable_drag_reorder ?? true + } +} + +/** + * Transform frontend config to API request format + */ +function toApiRequest(config: Partial): Partial { + const req: Partial = {} + if (config.disabledCardIds !== undefined) req.disabled_card_ids = config.disabledCardIds + if (config.rowOrder !== undefined) req.row_order = config.rowOrder + if (config.enableDragReorder !== undefined) req.enable_drag_reorder = config.enableDragReorder + return req +} + +/** + * localStorage fallback when backend is not ready + */ +function getLocalConfig(): FarmDashboardConfig | null { + if (typeof window === 'undefined') return null + try { + const stored = localStorage.getItem(STORAGE_KEY) + return stored ? (JSON.parse(stored) as FarmDashboardConfig) : null + } catch { + return null + } +} + +function setLocalConfig(config: FarmDashboardConfig): void { + if (typeof window === 'undefined') return + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)) + } catch (e) { + console.error('Failed to save farm dashboard config to localStorage', e) + } +} + +export const farmDashboardService = { + /** + * Get farm dashboard config for current user + */ + async getConfig(): Promise { + try { + const response = await apiClient.get< + ApiResponse | FarmDashboardConfigResponse + >('/api/farm-dashboard-config') + const raw = response && 'data' in response ? response.data : response + if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) { + return fromApiResponse(raw as FarmDashboardConfigResponse) + } + throw new Error('Invalid response') + } catch { + const local = getLocalConfig() + if (local) return local + return { disabledCardIds: [], rowOrder: [], enableDragReorder: true } + } + }, + + /** + * Update farm dashboard config + */ + async updateConfig(data: Partial): Promise { + try { + const response = await apiClient.patch< + ApiResponse | FarmDashboardConfigResponse + >('/api/farm-dashboard-config', toApiRequest(data)) + const raw = response && 'data' in response ? response.data : response + if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) { + const config = fromApiResponse(raw as FarmDashboardConfigResponse) + setLocalConfig(config) + return config + } + throw new Error('Update failed') + } catch (err) { + const local = getLocalConfig() + if (local) { + const merged: FarmDashboardConfig = { + disabledCardIds: data.disabledCardIds ?? local.disabledCardIds, + rowOrder: data.rowOrder ?? local.rowOrder, + enableDragReorder: data.enableDragReorder ?? local.enableDragReorder ?? true + } + setLocalConfig(merged) + return merged + } + throw err + } + } +} diff --git a/src/views/dashboards/farm/AnomalyDetectionCard.tsx b/src/views/dashboards/farm/AnomalyDetectionCard.tsx new file mode 100644 index 0000000..51bdae3 --- /dev/null +++ b/src/views/dashboards/farm/AnomalyDetectionCard.tsx @@ -0,0 +1,66 @@ +'use client' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +type AnomalyItem = { + sensor: string + value: string + expected: string + deviation: string + severity: 'warning' | 'error' +} + +const anomalies: AnomalyItem[] = [ + { sensor: 'Soil Moisture Z3', value: '38%', expected: '45-65%', deviation: '-12%', severity: 'warning' }, + { sensor: 'pH Sector 2', value: '5.2', expected: '6.0-7.0', deviation: '-0.8', severity: 'error' } +] + +const AnomalyDetectionCard = () => { + return ( + + } + title='Anomaly Detection' + subheader='Out of range values' + action={} + sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }} + /> + + {anomalies.length === 0 ? ( + + No anomalies detected. All sensors within optimal range. + + ) : ( + anomalies.map((item, index) => ( +
+
+ + {item.sensor} + + +
+ + Value: {item.value} (Expected: {item.expected}) · Deviation: {item.deviation} + +
+ )) + )} +
+
+ ) +} + +export default AnomalyDetectionCard diff --git a/src/views/dashboards/farm/EconomicOverview.tsx b/src/views/dashboards/farm/EconomicOverview.tsx new file mode 100644 index 0000000..7df9717 --- /dev/null +++ b/src/views/dashboards/farm/EconomicOverview.tsx @@ -0,0 +1,114 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Grid from '@mui/material/Grid2' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import classnames from 'classnames' +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - Cost breakdown (stacked bar style) +const series = [ + { name: 'Water Cost', data: [120, 115, 110, 125, 118, 122] }, + { name: 'Fertilizer', data: [80, 85, 90, 75, 82, 78] } +] + +type EconomicItem = { + title: string + value: string + subtitle: string + avatarIcon: string + avatarColor: 'primary' | 'success' | 'info' | 'warning' +} + +const economicData: EconomicItem[] = [ + { title: 'Water Cost', value: '€720', subtitle: 'This month', avatarIcon: 'tabler-droplet', avatarColor: 'primary' }, + { title: 'AI Water Savings', value: '€156', subtitle: '18% saved', avatarIcon: 'tabler-bulb', avatarColor: 'success' }, + { title: 'Platform ROI', value: '127%', subtitle: 'vs last year', avatarIcon: 'tabler-chart-line', avatarColor: 'info' }, + { title: 'Income Forecast', value: '€42k', subtitle: 'This season', avatarIcon: 'tabler-currency-euro', avatarColor: 'success' } +] + +const EconomicOverview = () => { + const theme = useTheme() + + const options: ApexOptions = { + chart: { + stacked: true, + parentHeightOffset: 0, + toolbar: { show: false } + }, + legend: { position: 'top', labels: { colors: 'var(--mui-palette-text-secondary)' } }, + dataLabels: { enabled: false }, + stroke: { width: 5, colors: ['var(--mui-palette-background-paper)'] }, + colors: ['var(--mui-palette-primary-main)', 'var(--mui-palette-info-main)'], + plotOptions: { + bar: { + borderRadius: 7, + columnWidth: '50%', + borderRadiusApplication: 'around' + } + }, + grid: { + borderColor: 'var(--mui-palette-divider)', + yaxis: { lines: { show: false } }, + padding: { top: -40, left: -10, right: 0, bottom: -15 } + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + labels: { show: false }, + axisTicks: { show: false }, + axisBorder: { show: false } + }, + yaxis: { labels: { show: false } } + } + + return ( + + } + /> + + + {economicData.map((item, index) => ( + +
+ + + +
+ {item.value} + + {item.title} + + + {item.subtitle} + +
+
+
+ ))} +
+ +
+
+ ) +} + +export default EconomicOverview diff --git a/src/views/dashboards/farm/FarmAlertsTimeline.tsx b/src/views/dashboards/farm/FarmAlertsTimeline.tsx new file mode 100644 index 0000000..3f1938c --- /dev/null +++ b/src/views/dashboards/farm/FarmAlertsTimeline.tsx @@ -0,0 +1,104 @@ +'use client' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { styled } from '@mui/material/styles' +import TimelineDot from '@mui/lab/TimelineDot' +import TimelineItem from '@mui/lab/TimelineItem' +import TimelineContent from '@mui/lab/TimelineContent' +import TimelineSeparator from '@mui/lab/TimelineSeparator' +import TimelineConnector from '@mui/lab/TimelineConnector' +import MuiTimeline from '@mui/lab/Timeline' +import type { TimelineProps } from '@mui/lab/Timeline' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Timeline +const Timeline = styled(MuiTimeline)({ + paddingLeft: 0, + paddingRight: 0, + '& .MuiTimelineItem-root': { + width: '100%', + '&:before': { display: 'none' } + } +}) + +type AlertItem = { + title: string + description: string + time: string + color: 'primary' | 'warning' | 'error' | 'info' | 'success' +} + +const alerts: AlertItem[] = [ + { + title: 'Water Shortage Risk', + description: + 'Soil moisture at 10cm depth (42%) is below optimal. AI predicts stress in 2-3 days if no irrigation. Recommended: irrigate within 24h.', + time: '15 min ago', + color: 'warning' + }, + { + title: 'Fungal Disease Risk', + description: + 'High humidity (65%) + temp 24°C creates favorable conditions for fungal growth. Consider preventive fungicide or reduce irrigation.', + time: '1 hour ago', + color: 'error' + }, + { + title: 'Irrigation Suggestion', + description: 'Optimal watering window: 6:00-8:00 AM. Suggested amount: 450 m³ for Zone A. Expected efficiency gain: 12%.', + time: '2 hours ago', + color: 'info' + }, + { + title: 'Soil Salinity Check', + description: 'EC reading 1.2 dS/m is within range. No action needed. Next check recommended in 5 days.', + time: '4 hours ago', + color: 'success' + } +] + +const FarmAlertsTimeline = () => { + return ( + + } + title='AI Alerts' + titleTypographyProps={{ variant: 'h5' }} + subheader='Explainable recommendations' + action={} + sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }} + /> + + + {alerts.map((alert, index) => ( + + + + {index < alerts.length - 1 && } + + +
+ + {alert.title} + + {alert.time} +
+ + {alert.description} + +
+
+ ))} +
+
+
+ ) +} + +export default FarmAlertsTimeline diff --git a/src/views/dashboards/farm/FarmAlertsTracker.tsx b/src/views/dashboards/farm/FarmAlertsTracker.tsx new file mode 100644 index 0000000..5365ee2 --- /dev/null +++ b/src/views/dashboards/farm/FarmAlertsTracker.tsx @@ -0,0 +1,128 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/material/styles' + +// Third Party Imports +import classnames from 'classnames' +import type { ApexOptions } from 'apexcharts' + +// Types Imports +import type { ThemeColor } from '@core/types' + +// Components Imports +import OptionMenu from '@core/components/option-menu' +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +type AlertStatType = { + title: string + subtitle: string + avatarIcon: string + avatarColor?: ThemeColor +} + +const data: AlertStatType[] = [ + { title: 'Water Shortage', subtitle: '2', avatarColor: 'error', avatarIcon: 'tabler-droplet-half-2' }, + { title: 'Fungal Risk', subtitle: '1', avatarColor: 'warning', avatarIcon: 'tabler-mushroom' }, + { title: 'Frost Alert', subtitle: '0', avatarColor: 'info', avatarIcon: 'tabler-snowflake' } +] + +const FarmAlertsTracker = () => { + const theme = useTheme() + const disabledText = 'var(--mui-palette-text-disabled)' + + const options: ApexOptions = { + stroke: { dashArray: 10 }, + labels: ['Active Alerts'], + colors: ['var(--mui-palette-warning-main)'], + states: { + hover: { filter: { type: 'none' } }, + active: { filter: { type: 'none' } } + }, + fill: { + type: 'gradient', + gradient: { + shade: 'dark', + opacityTo: 0.5, + opacityFrom: 1, + shadeIntensity: 0.5, + stops: [30, 70, 100], + inverseColors: false, + gradientToColors: ['var(--mui-palette-warning-main)'] + } + }, + plotOptions: { + radialBar: { + endAngle: 130, + startAngle: -140, + hollow: { size: '60%' }, + track: { background: 'transparent' }, + dataLabels: { + name: { + offsetY: -24, + color: disabledText, + fontFamily: theme.typography.fontFamily, + fontSize: theme.typography.body2.fontSize as string + }, + value: { + offsetY: 8, + fontWeight: 500, + formatter: () => '3', + color: 'var(--mui-palette-text-primary)', + fontFamily: theme.typography.fontFamily, + fontSize: theme.typography.h2.fontSize as string + } + } + } + }, + grid: { + padding: { top: -18, left: 0, right: 0, bottom: 14 } + } + } + + return ( + + } + /> + +
+
+ 3 + Total Alerts +
+
+ {data.map((item, index) => ( +
+ + + +
+ + {item.title} + + {item.subtitle} +
+
+ ))} +
+
+ +
+
+ ) +} + +export default FarmAlertsTracker diff --git a/src/views/dashboards/farm/FarmDashboardSettingsDrawer.tsx b/src/views/dashboards/farm/FarmDashboardSettingsDrawer.tsx new file mode 100644 index 0000000..77b4931 --- /dev/null +++ b/src/views/dashboards/farm/FarmDashboardSettingsDrawer.tsx @@ -0,0 +1,99 @@ +'use client' + +// MUI Imports +import Drawer from '@mui/material/Drawer' +import Typography from '@mui/material/Typography' +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' +import Divider from '@mui/material/Divider' +import Box from '@mui/material/Box' + +// Component Imports +import PerfectScrollbar from 'react-perfect-scrollbar' + +// Config +import type { RowId, CardId } from '@views/dashboards/farm/farmDashboardConfig' +import { ROW_IDS } from '@views/dashboards/farm/farmDashboardConfig' + +type RowCardsType = Record +type RowLabelsType = Record +type CardLabelsType = Record + +type FarmDashboardSettingsDrawerProps = { + open: boolean + onClose: () => void + disabledCardIds: string[] + onToggleCard: (cardId: CardId, disabled: boolean) => void + cardLabels: CardLabelsType + rowLabels: RowLabelsType + rowCards: RowCardsType +} + +const FarmDashboardSettingsDrawer = (props: FarmDashboardSettingsDrawerProps) => { + const { open, onClose, disabledCardIds, onToggleCard, cardLabels, rowLabels, rowCards } = props + const disabledSet = new Set(disabledCardIds) + + return ( + + + + Dashboard Settings + + Toggle cards to show or hide on the dashboard + + + + + + {ROW_IDS.map(rowId => ( + + + {rowLabels[rowId]} + + {rowCards[rowId].map(cardId => { + const isDisabled = disabledSet.has(cardId) + return ( + + onToggleCard(cardId, !e.target.checked)} + size='small' + /> + } + label={} + sx={{ m: 0, width: '100%' }} + /> + + ) + })} + + + ))} + + + + + ) +} + +export default FarmDashboardSettingsDrawer diff --git a/src/views/dashboards/farm/FarmDashboardSettingsDropdown.tsx b/src/views/dashboards/farm/FarmDashboardSettingsDropdown.tsx new file mode 100644 index 0000000..a46b5a9 --- /dev/null +++ b/src/views/dashboards/farm/FarmDashboardSettingsDropdown.tsx @@ -0,0 +1,152 @@ +'use client' + +// React Imports +import { useRef, useState } from 'react' + +// MUI Imports +import IconButton from '@mui/material/IconButton' +import Popper from '@mui/material/Popper' +import Fade from '@mui/material/Fade' +import Paper from '@mui/material/Paper' +import ClickAwayListener from '@mui/material/ClickAwayListener' +import Typography from '@mui/material/Typography' +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' +import Switch from '@mui/material/Switch' +import Divider from '@mui/material/Divider' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' + +// Hook Imports +import { useSettings } from '@core/hooks/useSettings' + +// Config +import type { RowId, CardId } from '@views/dashboards/farm/farmDashboardConfig' +import { ROW_IDS } from '@views/dashboards/farm/farmDashboardConfig' + +type RowCardsType = Record +type RowLabelsType = Record +type CardLabelsType = Record + +type FarmDashboardSettingsDropdownProps = { + disabledCardIds: string[] + onToggleCard: (cardId: CardId, disabled: boolean) => void + enableDragReorder: boolean + onToggleDragReorder: (enabled: boolean) => void + cardLabels: CardLabelsType + rowLabels: RowLabelsType + rowCards: RowCardsType + saving?: boolean +} + +const FarmDashboardSettingsDropdown = (props: FarmDashboardSettingsDropdownProps) => { + const { disabledCardIds, onToggleCard, enableDragReorder, onToggleDragReorder, cardLabels, rowLabels, rowCards, saving } = props + const { settings } = useSettings() + const [open, setOpen] = useState(false) + const anchorRef = useRef(null) + const disabledSet = new Set(disabledCardIds) + + const handleClose = () => setOpen(false) + const handleToggle = () => setOpen(prev => !prev) + + return ( + <> + + {saving && } + + + + + + {({ TransitionProps, placement }) => ( + + + + + + Dashboard Settings + + Toggle cards to show or hide + + + + onToggleDragReorder(e.target.checked)} + size='small' + /> + } + label={ + Enable drag & reorder rows + } + sx={{ m: 0 }} + /> + + + + {ROW_IDS.map(rowId => ( + + + {rowLabels[rowId]} + + {rowCards[rowId].map(cardId => { + const isDisabled = disabledSet.has(cardId) + return ( + + onToggleCard(cardId, !e.target.checked)} + size='small' + /> + } + label={ + + } + sx={{ m: 0, width: '100%' }} + /> + + ) + })} + + ))} + + + + + + )} + + + ) +} + +export default FarmDashboardSettingsDropdown diff --git a/src/views/dashboards/farm/FarmDashboardWrapper.tsx b/src/views/dashboards/farm/FarmDashboardWrapper.tsx new file mode 100644 index 0000000..cfbd9ec --- /dev/null +++ b/src/views/dashboards/farm/FarmDashboardWrapper.tsx @@ -0,0 +1,253 @@ +'use client' + +// React Imports +import type { RefObject } from 'react' +import { useEffect, useState, useCallback, useContext } from 'react' + +// Context Imports +import NavbarSlotContext from '@/contexts/navbarSlotContext' + +// MUI Imports +import Grid from '@mui/material/Grid2' +import IconButton from '@mui/material/IconButton' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' + +// Third-party imports +import { useDragAndDrop } from '@formkit/drag-and-drop/react' +import { animations } from '@formkit/drag-and-drop' + +// Component Imports +import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs' +import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard' +import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker' +import SensorValuesList from '@views/dashboards/farm/SensorValuesList' +import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart' +import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart' +import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline' +import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction' +import YieldPredictionChart from '@views/dashboards/farm/YieldPredictionChart' +import HarvestPredictionCard from '@views/dashboards/farm/HarvestPredictionCard' +import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap' +import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard' +import NDVIHealthCard from '@views/dashboards/farm/NDVIHealthCard' +import RecommendationsList from '@views/dashboards/farm/RecommendationsList' +import EconomicOverview from '@views/dashboards/farm/EconomicOverview' + +// Config & Service +import { + ROW_IDS, + ROW_CARDS, + ROW_LABELS, + CARD_GRID_SIZE, + CARD_LABELS, + DEFAULT_FARM_DASHBOARD_CONFIG, + type RowId, + type CardId, + type FarmDashboardConfig +} from '@views/dashboards/farm/farmDashboardConfig' +import { farmDashboardService } from '@/libs/api/services/farmDashboardService' +import FarmDashboardSettingsDropdown from '@views/dashboards/farm/FarmDashboardSettingsDropdown' + +const cardRowSx = { + display: 'flex', + flexDirection: 'column', + '& > *': { flex: 1, minHeight: 0 } +} + +const CARD_COMPONENTS: Record = { + farmOverviewKpis: FarmOverviewKPIs, + farmWeatherCard: FarmWeatherCard, + farmAlertsTracker: FarmAlertsTracker, + sensorValuesList: SensorValuesList, + sensorRadarChart: SensorRadarChart, + sensorComparisonChart: SensorComparisonChart, + anomalyDetectionCard: AnomalyDetectionCard, + farmAlertsTimeline: FarmAlertsTimeline, + waterNeedPrediction: WaterNeedPrediction, + harvestPredictionCard: HarvestPredictionCard, + yieldPredictionChart: YieldPredictionChart, + soilMoistureHeatmap: SoilMoistureHeatmap, + ndviHealthCard: NDVIHealthCard, + recommendationsList: RecommendationsList, + economicOverview: EconomicOverview +} + +function mergeRowOrderAfterDrag( + currentRowOrder: string[], + newVisibleOrder: string[], + visibleRows: string[] +): string[] { + const result = [...currentRowOrder] + let visibleIndex = 0 + for (let i = 0; i < result.length; i++) { + if (visibleRows.includes(result[i])) { + result[i] = newVisibleOrder[visibleIndex++] + } + } + return result +} + +const FarmDashboardWrapper = () => { + const { setSlotContent } = useContext(NavbarSlotContext) + const [config, setConfig] = useState(DEFAULT_FARM_DASHBOARD_CONFIG) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + const disabledSet = new Set(config.disabledCardIds) + + const hasVisibleCard = useCallback( + (rowId: RowId) => ROW_CARDS[rowId].some(cardId => !disabledSet.has(cardId)), + [config.disabledCardIds] + ) + + const visibleRowOrder = config.rowOrder.filter(hasVisibleCard) + + const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(visibleRowOrder, { + plugins: [animations()], + dragHandle: '.row-drag-handle' + }) + + useEffect(() => { + farmDashboardService + .getConfig() + .then(data => { + const merged: FarmDashboardConfig = { + disabledCardIds: data.disabledCardIds ?? [], + rowOrder: data.rowOrder?.length ? data.rowOrder : [...ROW_IDS], + enableDragReorder: data.enableDragReorder ?? true + } + setConfig(merged) + }) + .catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG)) + .finally(() => setLoading(false)) + }, []) + + useEffect(() => { + setOrderedRows(visibleRowOrder) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.disabledCardIds]) + + + useEffect(() => { + if (loading) return + if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return + const newRowOrder = mergeRowOrderAfterDrag(config.rowOrder, orderedRows, visibleRowOrder) + setConfig(prev => ({ ...prev, rowOrder: newRowOrder })) + setSaving(true) + farmDashboardService + .updateConfig({ rowOrder: newRowOrder }) + .then(updated => setConfig(updated)) + .catch(() => {}) + .finally(() => setSaving(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [orderedRows]) + + const handleToggleDragReorder = useCallback((enabled: boolean) => { + setConfig(prev => ({ ...prev, enableDragReorder: enabled })) + setSaving(true) + farmDashboardService + .updateConfig({ enableDragReorder: enabled }) + .then(updated => setConfig(updated)) + .finally(() => setSaving(false)) + }, []) + + const handleToggleCard = useCallback( + (cardId: CardId, disabled: boolean) => { + const next = disabled + ? [...config.disabledCardIds, cardId] + : config.disabledCardIds.filter(id => id !== cardId) + setConfig(prev => ({ ...prev, disabledCardIds: next })) + setSaving(true) + farmDashboardService + .updateConfig({ disabledCardIds: next }) + .then(updated => setConfig(updated)) + .catch(() => setConfig(prev => ({ ...prev, disabledCardIds: next }))) + .finally(() => setSaving(false)) + }, + [config.disabledCardIds] + ) + + useEffect(() => { + setSlotContent( + + ) + return () => setSlotContent(null) + }, [setSlotContent, config.disabledCardIds, config.enableDragReorder, handleToggleCard, handleToggleDragReorder, saving]) + + if (loading) { + return ( + + + + ) + } + + return ( + + }> + {orderedRows.map((rowId: string) => { + const cards = ROW_CARDS[rowId as RowId].filter(cardId => !disabledSet.has(cardId)) + if (cards.length === 0) return null + + const isOverviewRow = rowId === 'overviewKpis' + + return ( + + {config.enableDragReorder !== false && ( + + + + )} + + {isOverviewRow && cards.includes('farmOverviewKpis') && } + {!isOverviewRow && + cards.map((cardId: CardId) => { + const size = CARD_GRID_SIZE[cardId] + const Component = CARD_COMPONENTS[cardId] + if (!Component) return null + return ( + + + + ) + })} + + + ) + })} + + + ) +} + +export default FarmDashboardWrapper diff --git a/src/views/dashboards/farm/FarmOverviewKPIs.tsx b/src/views/dashboards/farm/FarmOverviewKPIs.tsx new file mode 100644 index 0000000..927a418 --- /dev/null +++ b/src/views/dashboards/farm/FarmOverviewKPIs.tsx @@ -0,0 +1,100 @@ +'use client' + +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Component Imports +import CardStatsVertical from '@components/card-statistics/Vertical' + +const FarmOverviewKPIs = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ) +} + +export default FarmOverviewKPIs diff --git a/src/views/dashboards/farm/FarmWeatherCard.tsx b/src/views/dashboards/farm/FarmWeatherCard.tsx new file mode 100644 index 0000000..1f7e5b4 --- /dev/null +++ b/src/views/dashboards/farm/FarmWeatherCard.tsx @@ -0,0 +1,98 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import Typography from '@mui/material/Typography' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - Mock weather data (temp variation through day) +const series = [{ data: [18, 22, 26, 28, 25, 20, 18] }] + +const FarmWeatherCard = () => { + const theme = useTheme() + const infoColor = theme.palette.info.main + + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + sparkline: { enabled: true } + }, + tooltip: { enabled: false }, + dataLabels: { enabled: false }, + stroke: { + width: 2, + curve: 'smooth' + }, + grid: { + show: false, + padding: { bottom: 20 } + }, + fill: { + type: 'gradient', + gradient: { + opacityTo: 0, + opacityFrom: 1, + shadeIntensity: 1, + stops: [0, 100], + colorStops: [ + [ + { offset: 0, opacity: 0.4, color: infoColor }, + { opacity: 0, offset: 100, color: 'var(--mui-palette-background-paper)' } + ] + ] + } + }, + theme: { + monochrome: { + enabled: true, + shadeTo: 'light', + shadeIntensity: 1, + color: infoColor + } + }, + xaxis: { + labels: { show: false }, + axisTicks: { show: false }, + axisBorder: { show: false } + }, + yaxis: { show: false } + } + + return ( + + } + /> + +
+ + 24°C + + Humid: 45% | Wind: 12 km/h + +
+
+ +
+ ) +} + +export default FarmWeatherCard diff --git a/src/views/dashboards/farm/HarvestPredictionCard.tsx b/src/views/dashboards/farm/HarvestPredictionCard.tsx new file mode 100644 index 0000000..7f940a5 --- /dev/null +++ b/src/views/dashboards/farm/HarvestPredictionCard.tsx @@ -0,0 +1,40 @@ +'use client' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' + +// Component Imports +import CustomAvatar from '@core/components/mui/Avatar' +import OptionMenu from '@core/components/option-menu' + +const HarvestPredictionCard = () => { + return ( + + + + + } + title='Harvest Prediction' + subheader='AI Estimated Date' + action={} + /> + +
+ Oct 15, 2025 + +
+ + Based on current GDD accumulation and weather forecast. Optimal harvest window: Oct 12-18. + +
+
+ ) +} + +export default HarvestPredictionCard diff --git a/src/views/dashboards/farm/NDVIHealthCard.tsx b/src/views/dashboards/farm/NDVIHealthCard.tsx new file mode 100644 index 0000000..5b78fa3 --- /dev/null +++ b/src/views/dashboards/farm/NDVIHealthCard.tsx @@ -0,0 +1,112 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import classnames from 'classnames' +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +const NDVIHealthCard = () => { + const theme = useTheme() + const successColor = theme.palette.success.main + const disabledText = 'var(--mui-palette-text-disabled)' + + const options: ApexOptions = { + stroke: { dashArray: 10 }, + labels: ['NDVI'], + colors: [successColor], + states: { + hover: { filter: { type: 'none' } }, + active: { filter: { type: 'none' } } + }, + fill: { + type: 'gradient', + gradient: { + shade: 'dark', + opacityTo: 0.5, + opacityFrom: 1, + shadeIntensity: 0.5, + stops: [30, 70, 100], + inverseColors: false, + gradientToColors: [successColor] + } + }, + plotOptions: { + radialBar: { + endAngle: 130, + startAngle: -140, + hollow: { size: '60%' }, + track: { background: 'transparent' }, + dataLabels: { + name: { + offsetY: -24, + color: disabledText, + fontFamily: theme.typography.fontFamily, + fontSize: theme.typography.body2.fontSize as string + }, + value: { + offsetY: 8, + fontWeight: 500, + formatter: (val: number) => val.toFixed(2), + color: 'var(--mui-palette-text-primary)', + fontFamily: theme.typography.fontFamily, + fontSize: theme.typography.h2.fontSize as string + } + } + } + } + } + + const healthData = [ + { title: 'Nitrogen Stress', value: 'Low', color: 'success', icon: 'tabler-leaf' }, + { title: 'Crop Health', value: 'Good', color: 'success', icon: 'tabler-plant' } + ] + + return ( + + } + title='NDVI Health' + subheader='Vegetation Index' + sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }} + /> + +
+ 0.78 + NDVI Index (0-1) +
+ {healthData.map((item, index) => ( +
+ + + +
+ {item.title} + +
+
+ ))} +
+
+ +
+
+ ) +} + +export default NDVIHealthCard diff --git a/src/views/dashboards/farm/RecommendationsList.tsx b/src/views/dashboards/farm/RecommendationsList.tsx new file mode 100644 index 0000000..e2921cd --- /dev/null +++ b/src/views/dashboards/farm/RecommendationsList.tsx @@ -0,0 +1,79 @@ +'use client' + +// MUI Imports +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 classnames from 'classnames' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import CustomAvatar from '@core/components/mui/Avatar' + +type RecommendationType = { + title: string + subtitle: string + avatarIcon: string + avatarColor: 'primary' | 'info' | 'success' | 'warning' | 'error' +} + +const data: RecommendationType[] = [ + { + title: 'Irrigation: 6:00-8:00 AM', + subtitle: '450 m³ for Zone A. Without irrigation, yield may drop ~8%.', + avatarIcon: 'tabler-droplet', + avatarColor: 'primary' + }, + { + title: 'Fertilizer: NPK 20-20-20', + subtitle: 'Apply 25 kg/ha in 7 days. Current N deficiency in sector 2.', + avatarIcon: 'tabler-leaf', + avatarColor: 'success' + }, + { + title: 'Fungicide: Preventive', + subtitle: 'Humidity + temp favor fungi. Consider copper-based spray.', + avatarIcon: 'tabler-mushroom', + avatarColor: 'warning' + }, + { + title: 'Harvest Window: Oct 12-18', + subtitle: 'Peak ripeness expected Oct 15. Plan labor accordingly.', + avatarIcon: 'tabler-calendar-event', + avatarColor: 'info' + } +] + +const RecommendationsList = () => { + return ( + + } + /> + + {data.map((item, index) => ( +
+ + + +
+ + {item.title} + + + {item.subtitle} + +
+
+ ))} +
+
+ ) +} + +export default RecommendationsList diff --git a/src/views/dashboards/farm/SensorComparisonChart.tsx b/src/views/dashboards/farm/SensorComparisonChart.tsx new file mode 100644 index 0000000..18024d8 --- /dev/null +++ b/src/views/dashboards/farm/SensorComparisonChart.tsx @@ -0,0 +1,91 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - Soil moisture: today vs last week (7 days) +const series = [ + { name: 'Today', data: [42, 45, 48, 52, 50, 48, 46] }, + { name: 'Last Week', data: [38, 40, 42, 45, 43, 40, 38] } +] + +const SensorComparisonChart = () => { + const theme = useTheme() + + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + zoom: { enabled: false } + }, + colors: ['var(--mui-palette-primary-main)', 'var(--mui-palette-info-main)'], + stroke: { width: 2, curve: 'smooth' }, + legend: { + position: 'top', + labels: { colors: 'var(--mui-palette-text-secondary)' }, + markers: { offsetX: theme.direction === 'rtl' ? 7 : -4 } + }, + dataLabels: { enabled: false }, + grid: { + borderColor: 'var(--mui-palette-divider)', + strokeDashArray: 4, + xaxis: { lines: { show: false } }, + yaxis: { lines: { show: true } } + }, + xaxis: { + categories: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + labels: { + style: { colors: 'var(--mui-palette-text-disabled)' } + }, + axisBorder: { show: false }, + axisTicks: { show: false } + }, + yaxis: { + labels: { + style: { colors: 'var(--mui-palette-text-disabled)' }, + formatter: (val: number) => `${val}%` + } + }, + fill: { + type: 'gradient', + gradient: { + opacityFrom: 0.4, + opacityTo: 0.05, + shadeIntensity: 0.5 + } + } + } + + return ( + + + +
+ 48% + + +5% vs last week + +
+ +
+
+ ) +} + +export default SensorComparisonChart diff --git a/src/views/dashboards/farm/SensorRadarChart.tsx b/src/views/dashboards/farm/SensorRadarChart.tsx new file mode 100644 index 0000000..eb561e1 --- /dev/null +++ b/src/views/dashboards/farm/SensorRadarChart.tsx @@ -0,0 +1,85 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import { useTheme } from '@mui/material/styles' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - Today vs ideal ranges (normalized 0-100) +const series = [ + { name: 'Today', data: [75, 65, 80, 70, 85, 60] }, + { name: 'Ideal', data: [80, 70, 75, 75, 90, 50] } +] + +const labels = ['Temp', 'Humidity', 'pH', 'EC', 'Light', 'Wind'] + +const SensorRadarChart = () => { + const theme = useTheme() + const textDisabled = 'var(--mui-palette-text-disabled)' + const divider = 'var(--mui-palette-divider)' + + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false } + }, + colors: ['var(--mui-palette-primary-main)', 'var(--mui-palette-success-main)'], + plotOptions: { + radar: { + polygons: { + connectorColors: divider, + strokeColors: divider + } + } + }, + stroke: { width: 0 }, + fill: { opacity: [1, 0.5] }, + labels, + markers: { size: 0 }, + legend: { + fontSize: '13px', + labels: { colors: 'var(--mui-palette-text-secondary)' }, + markers: { offsetY: -1, offsetX: theme.direction === 'rtl' ? 7 : -4 }, + itemMargin: { horizontal: 9 } + }, + grid: { show: false }, + xaxis: { + labels: { + show: true, + style: { + fontSize: '13px', + colors: Array(6).fill(textDisabled) + } + } + }, + yaxis: { show: false } + } + + return ( + + } + /> + + + + + ) +} + +export default SensorRadarChart diff --git a/src/views/dashboards/farm/SensorValuesList.tsx b/src/views/dashboards/farm/SensorValuesList.tsx new file mode 100644 index 0000000..c55ec0f --- /dev/null +++ b/src/views/dashboards/farm/SensorValuesList.tsx @@ -0,0 +1,72 @@ +'use client' + +// MUI Imports +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 classnames from 'classnames' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +type SensorDataType = { + title: string + subtitle: string + trendNumber: number + trend?: 'positive' | 'negative' + unit: string +} + +const data: SensorDataType[] = [ + { title: '28°C', subtitle: 'Air Temperature', trendNumber: 2.1, unit: '°C' }, + { title: '24°C', subtitle: 'Soil Temperature', trendNumber: -0.5, trend: 'negative', unit: '°C' }, + { title: '65%', subtitle: 'Air Humidity', trendNumber: 3.2, unit: '%' }, + { title: '42%', subtitle: 'Soil Moisture (10cm)', trendNumber: -1.8, trend: 'negative', unit: '%' }, + { title: '6.8', subtitle: 'Soil pH', trendNumber: 0.2, unit: 'pH' }, + { title: '1.2', subtitle: 'EC (dS/m)', trendNumber: 0.1, unit: 'dS/m' }, + { title: '850', subtitle: 'Light Intensity (lux)', trendNumber: 15.3, unit: 'lux' }, + { title: '12', subtitle: 'Wind Speed (km/h)', trendNumber: -2.4, trend: 'negative', unit: 'km/h' } +] + +const SensorValuesList = () => { + return ( + + } + /> + + {data.map((item, index) => ( +
+
+
+ + {item.title} + + {item.subtitle} +
+
+ + {`${item.trendNumber > 0 ? '+' : ''}${item.trendNumber}%`} +
+
+
+ ))} +
+
+ ) +} + +export default SensorValuesList diff --git a/src/views/dashboards/farm/SoilMoistureHeatmap.tsx b/src/views/dashboards/farm/SoilMoistureHeatmap.tsx new file mode 100644 index 0000000..66b3ed8 --- /dev/null +++ b/src/views/dashboards/farm/SoilMoistureHeatmap.tsx @@ -0,0 +1,81 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Generate soil moisture data: rows = field zones (Z1-Z7), cols = hours (6am-6pm) +const zones = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'Z7'] +const hours = ['6h', '8h', '10h', '12h', '14h', '16h', '18h'] + +const series = zones.map((zone, i) => ({ + name: zone, + data: hours.map((h, j) => ({ + x: h, + y: Math.floor(Math.random() * 40) + 35 + })) +})) + +const SoilMoistureHeatmap = () => { + const theme = useTheme() + + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false } + }, + dataLabels: { enabled: false }, + legend: { + position: 'bottom', + labels: { colors: 'var(--mui-palette-text-secondary)' }, + markers: { offsetX: theme.direction === 'rtl' ? 7 : -4 } + }, + plotOptions: { + heatmap: { + enableShades: false, + colorScale: { + ranges: [ + { from: 0, to: 30, name: 'Low', color: '#ff6b6b' }, + { from: 31, to: 50, name: 'Moderate', color: '#ffd93d' }, + { from: 51, to: 70, name: 'Optimal', color: '#6bcb77' }, + { from: 71, to: 100, name: 'High', color: '#4d96ff' } + ] + } + } + }, + grid: { padding: { top: -20 } }, + xaxis: { + labels: { show: true, style: { colors: 'var(--mui-palette-text-disabled)', fontSize: '11px' } } + }, + yaxis: { + labels: { + style: { colors: 'var(--mui-palette-text-disabled)', fontSize: '13px' } + } + } + } + + return ( + + + + + + + ) +} + +export default SoilMoistureHeatmap diff --git a/src/views/dashboards/farm/WaterNeedPrediction.tsx b/src/views/dashboards/farm/WaterNeedPrediction.tsx new file mode 100644 index 0000000..6186438 --- /dev/null +++ b/src/views/dashboards/farm/WaterNeedPrediction.tsx @@ -0,0 +1,87 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - 7-day water need prediction (m³) +const series = [{ name: 'Water Need', data: [420, 450, 480, 460, 490, 510, 480] }] + +const WaterNeedPrediction = () => { + const theme = useTheme() + const primaryColor = theme.palette.primary.main + + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false } + }, + stroke: { width: 2, curve: 'smooth' }, + colors: [primaryColor], + fill: { + type: 'gradient', + gradient: { + opacityFrom: 0.5, + opacityTo: 0.1, + shadeIntensity: 0.5 + } + }, + dataLabels: { enabled: false }, + grid: { + borderColor: 'var(--mui-palette-divider)', + strokeDashArray: 4, + xaxis: { lines: { show: false } } + }, + xaxis: { + categories: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'], + labels: { style: { colors: 'var(--mui-palette-text-disabled)' } }, + axisBorder: { show: false }, + axisTicks: { show: false } + }, + yaxis: { + labels: { + style: { colors: 'var(--mui-palette-text-disabled)' }, + formatter: (val: number) => `${val} m³` + } + }, + tooltip: { + y: { formatter: (val: number) => `${val} m³` } + } + } + + return ( + + } + /> + +
+ 3,290 m³ + + Total next 7 days + +
+ +
+
+ ) +} + +export default WaterNeedPrediction diff --git a/src/views/dashboards/farm/YieldPredictionChart.tsx b/src/views/dashboards/farm/YieldPredictionChart.tsx new file mode 100644 index 0000000..b2e39ce --- /dev/null +++ b/src/views/dashboards/farm/YieldPredictionChart.tsx @@ -0,0 +1,104 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/material/styles' + +// Third-party Imports +import classnames from 'classnames' +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars - Yield comparison: this year vs last year (tons per month) +const series = [ + { name: 'This Year', data: [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42] }, + { name: 'Last Year', data: [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38] } +] + +const YieldPredictionChart = () => { + const theme = useTheme() + + const options: ApexOptions = { + chart: { + stacked: false, + parentHeightOffset: 0, + toolbar: { show: false } + }, + legend: { + position: 'top', + labels: { colors: 'var(--mui-palette-text-secondary)' } + }, + stroke: { width: 2, curve: 'smooth' }, + colors: ['var(--mui-palette-primary-main)', 'var(--mui-palette-success-main)'], + dataLabels: { enabled: false }, + grid: { + borderColor: 'var(--mui-palette-divider)', + strokeDashArray: 4 + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + labels: { style: { colors: 'var(--mui-palette-text-disabled)' } } + }, + yaxis: { + labels: { + style: { colors: 'var(--mui-palette-text-disabled)' }, + formatter: (val: number) => `${val}t` + } + }, + tooltip: { + y: { formatter: (val: number) => `${val} tons` } + } + } + + const summaryData = [ + { title: 'Predicted Yield', subtitle: 'This Season', amount: '42 ton', avatarColor: 'primary', avatarIcon: 'tabler-chart-bar' }, + { title: 'Harvest Date', subtitle: 'Est. Oct 15', amount: '+8%', avatarColor: 'success', avatarIcon: 'tabler-calendar' } + ] + + return ( + + } + /> + + +
+ {summaryData.map((item, index) => ( +
+ + + +
+
+ + {item.title} + + {item.subtitle} +
+ + {item.amount} + +
+
+ ))} +
+
+
+ ) +} + +export default YieldPredictionChart diff --git a/src/views/dashboards/farm/farmDashboardConfig.ts b/src/views/dashboards/farm/farmDashboardConfig.ts new file mode 100644 index 0000000..7c45873 --- /dev/null +++ b/src/views/dashboards/farm/farmDashboardConfig.ts @@ -0,0 +1,134 @@ +/** + * Farm Dashboard - Config constants and type definitions + * Row IDs, Card IDs, and mapping for disable/reorder functionality + */ + +export const ROW_IDS = [ + 'overviewKpis', + 'weatherAlerts', + 'sensorMonitoring', + 'sensorCharts', + 'alertsWater', + 'predictions', + 'soilHeatmap', + 'ndviRecommendations', + 'economic' +] as const + +export type RowId = (typeof ROW_IDS)[number] + +export const CARD_IDS = [ + 'farmOverviewKpis', + 'farmWeatherCard', + 'farmAlertsTracker', + 'sensorValuesList', + 'sensorRadarChart', + 'sensorComparisonChart', + 'anomalyDetectionCard', + 'farmAlertsTimeline', + 'waterNeedPrediction', + 'harvestPredictionCard', + 'yieldPredictionChart', + 'soilMoistureHeatmap', + 'ndviHealthCard', + 'recommendationsList', + 'economicOverview' +] as const + +export type CardId = (typeof CARD_IDS)[number] + +/** Maps each card to its parent row */ +export const CARD_TO_ROW: Record = { + farmOverviewKpis: 'overviewKpis', + farmWeatherCard: 'weatherAlerts', + farmAlertsTracker: 'weatherAlerts', + sensorValuesList: 'sensorMonitoring', + sensorRadarChart: 'sensorMonitoring', + sensorComparisonChart: 'sensorCharts', + anomalyDetectionCard: 'sensorCharts', + farmAlertsTimeline: 'alertsWater', + waterNeedPrediction: 'alertsWater', + harvestPredictionCard: 'predictions', + yieldPredictionChart: 'predictions', + soilMoistureHeatmap: 'soilHeatmap', + ndviHealthCard: 'ndviRecommendations', + recommendationsList: 'ndviRecommendations', + economicOverview: 'economic' +} + +/** Grid size for each card - matches layout in page.tsx */ +export const CARD_GRID_SIZE: Record = { + farmOverviewKpis: { xs: 12, sm: 6, md: 4, lg: 12 }, // Full row of 6 KPIs - parent handles + farmWeatherCard: { xs: 12, md: 6, lg: 4 }, + farmAlertsTracker: { xs: 12, md: 6, lg: 8 }, + sensorValuesList: { xs: 12, lg: 5 }, + sensorRadarChart: { xs: 12, lg: 7 }, + sensorComparisonChart: { xs: 12, lg: 8 }, + anomalyDetectionCard: { xs: 12, lg: 4 }, + farmAlertsTimeline: { xs: 12, lg: 4 }, + waterNeedPrediction: { xs: 12, lg: 8 }, + harvestPredictionCard: { xs: 12, md: 6, lg: 4 }, + yieldPredictionChart: { xs: 12 }, + soilMoistureHeatmap: { xs: 12 }, + ndviHealthCard: { xs: 12, md: 6, lg: 4 }, + recommendationsList: { xs: 12, md: 6, lg: 8 }, + economicOverview: { xs: 12 } +} + +/** Display label for each card (for settings UI) */ +export const CARD_LABELS: Record = { + farmOverviewKpis: 'Overview KPIs', + farmWeatherCard: 'Weather', + farmAlertsTracker: 'Alerts Tracker', + sensorValuesList: 'Sensor Values', + sensorRadarChart: 'Sensor Radar Chart', + sensorComparisonChart: 'Sensor Comparison', + anomalyDetectionCard: 'Anomaly Detection', + farmAlertsTimeline: 'Alerts Timeline', + waterNeedPrediction: 'Water Need Prediction', + harvestPredictionCard: 'Harvest Prediction', + yieldPredictionChart: 'Yield Prediction', + soilMoistureHeatmap: 'Soil Moisture Heatmap', + ndviHealthCard: 'NDVI Health', + recommendationsList: 'Recommendations', + economicOverview: 'Economic Overview' +} + +/** Display label for each row (for drag handle / settings) */ +export const ROW_LABELS: Record = { + overviewKpis: 'Overview KPIs', + weatherAlerts: 'Weather & Alerts', + sensorMonitoring: 'Sensor Monitoring', + sensorCharts: 'Sensor Charts', + alertsWater: 'Alerts & Water Prediction', + predictions: 'Predictions', + soilHeatmap: 'Soil Moisture Heatmap', + ndviRecommendations: 'NDVI & Recommendations', + economic: 'Economic Overview' +} + +/** Cards that belong to each row (for rendering) */ +export const ROW_CARDS: Record = { + overviewKpis: ['farmOverviewKpis'], + weatherAlerts: ['farmWeatherCard', 'farmAlertsTracker'], + sensorMonitoring: ['sensorValuesList', 'sensorRadarChart'], + sensorCharts: ['sensorComparisonChart', 'anomalyDetectionCard'], + alertsWater: ['farmAlertsTimeline', 'waterNeedPrediction'], + predictions: ['harvestPredictionCard', 'yieldPredictionChart'], + soilHeatmap: ['soilMoistureHeatmap'], + ndviRecommendations: ['ndviHealthCard', 'recommendationsList'], + economic: ['economicOverview'] +} + +export interface FarmDashboardConfig { + disabledCardIds: string[] + rowOrder: string[] + enableDragReorder?: boolean +} + +/** Default config when no backend data */ +export const DEFAULT_FARM_DASHBOARD_CONFIG: FarmDashboardConfig = { + disabledCardIds: [], + rowOrder: [...ROW_IDS], + enableDragReorder: true +}