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 +}