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.

This commit is contained in:
2026-02-19 15:48:32 +03:30
parent 3871ec89f7
commit ec679c3287
27 changed files with 2183 additions and 0 deletions
@@ -0,0 +1,8 @@
// Components Imports
import FarmDashboardWrapper from '@views/dashboards/farm/FarmDashboardWrapper'
const DashboardFarm = async () => {
return <FarmDashboardWrapper />
}
export default DashboardFarm
+3
View File
@@ -3,6 +3,7 @@ import type { ChildrenType, Direction } from '@core/types'
// Context Imports // Context Imports
import { AuthProvider } from '@/contexts/authProvider' import { AuthProvider } from '@/contexts/authProvider'
import { NavbarSlotProvider } from '@/contexts/navbarSlotContext'
import { VerticalNavProvider } from '@menu/contexts/verticalNavContext' import { VerticalNavProvider } from '@menu/contexts/verticalNavContext'
import { SettingsProvider } from '@core/contexts/settingsContext' import { SettingsProvider } from '@core/contexts/settingsContext'
import ThemeProvider from '@components/theme' import ThemeProvider from '@components/theme'
@@ -29,6 +30,7 @@ const Providers = async (props: Props) => {
return ( return (
<AuthProvider> <AuthProvider>
<NavbarSlotProvider>
<VerticalNavProvider> <VerticalNavProvider>
<SettingsProvider settingsCookie={settingsCookie} mode={mode}> <SettingsProvider settingsCookie={settingsCookie} mode={mode}>
<ThemeProvider direction={direction} systemMode={systemMode}> <ThemeProvider direction={direction} systemMode={systemMode}>
@@ -37,6 +39,7 @@ const Providers = async (props: Props) => {
</ThemeProvider> </ThemeProvider>
</SettingsProvider> </SettingsProvider>
</VerticalNavProvider> </VerticalNavProvider>
</NavbarSlotProvider>
</AuthProvider> </AuthProvider>
) )
} }
@@ -1,5 +1,11 @@
'use client'
// Third-party Imports // Third-party Imports
import classnames from 'classnames' import classnames from 'classnames'
import { useContext } from 'react'
// Context Imports
import NavbarSlotContext from '@/contexts/navbarSlotContext'
// Type Imports // Type Imports
import type { ShortcutsType } from '@components/layout/shared/ShortcutsDropdown' import type { ShortcutsType } from '@components/layout/shared/ShortcutsDropdown'
@@ -106,11 +112,14 @@ const notifications: NotificationsType[] = [
] ]
const NavbarContent = () => { const NavbarContent = () => {
const { slotContent } = useContext(NavbarSlotContext)
return ( return (
<div className={classnames(verticalLayoutClasses.navbarContent, 'flex items-center justify-between gap-4 is-full')}> <div className={classnames(verticalLayoutClasses.navbarContent, 'flex items-center justify-between gap-4 is-full')}>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<NavToggle /> <NavToggle />
<NavSearch /> <NavSearch />
{slotContent}
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
<ModeDropdown/> <ModeDropdown/>
+35
View File
@@ -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<ReactNode>(null)
const setSlotContent = useCallback((content: ReactNode) => {
setSlotContentState(content)
}, [])
const value = useMemo(
() => ({
slotContent,
setSlotContent
}),
[slotContent, setSlotContent]
)
return <NavbarSlotContext.Provider value={value}>{children}</NavbarSlotContext.Provider>
}
export default NavbarSlotContext
@@ -28,6 +28,11 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [
label: 'logistics', label: 'logistics',
icon: 'tabler-truck', icon: 'tabler-truck',
href: '/dashboards/logistics' href: '/dashboards/logistics'
},
{
label: 'farm',
icon: 'tabler-plant-2',
href: '/dashboards/farm'
} }
] ]
}, },
+5
View File
@@ -32,6 +32,11 @@ const verticalMenuData = (): VerticalMenuDataType[] => [
label: 'logistics', label: 'logistics',
icon: 'tabler-circle', icon: 'tabler-circle',
href: '/dashboards/logistics' href: '/dashboards/logistics'
},
{
label: 'farm',
icon: 'tabler-plant-2',
href: '/dashboards/farm'
} }
] ]
}, },
+1
View File
@@ -15,4 +15,5 @@ export * from './services/todoService'
export * from './services/userManagementService' export * from './services/userManagementService'
export * from './services/rolesPermissionsService' export * from './services/rolesPermissionsService'
export * from './services/sensorHubService' export * from './services/sensorHubService'
export * from './services/farmDashboardService'
@@ -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<T> {
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<FarmDashboardConfig>): Partial<FarmDashboardConfigResponse> {
const req: Partial<FarmDashboardConfigResponse> = {}
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<FarmDashboardConfig> {
try {
const response = await apiClient.get<
ApiResponse<FarmDashboardConfigResponse> | 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<FarmDashboardConfig>): Promise<FarmDashboardConfig> {
try {
const response = await apiClient.patch<
ApiResponse<FarmDashboardConfigResponse> | 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
}
}
}
@@ -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 (
<Card>
<CardHeader
avatar={<i className='tabler-alert-triangle text-xl' />}
title='Anomaly Detection'
subheader='Out of range values'
action={<OptionMenu options={['View All', 'Configure', 'Export']} />}
sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }}
/>
<CardContent className='flex flex-col gap-3'>
{anomalies.length === 0 ? (
<Typography variant='body2' color='text.secondary'>
No anomalies detected. All sensors within optimal range.
</Typography>
) : (
anomalies.map((item, index) => (
<div key={index} className='flex flex-col gap-1 p-3 rounded bg-actionHover'>
<div className='flex items-center justify-between'>
<Typography variant='body2' className='font-medium'>
{item.sensor}
</Typography>
<Chip
label={item.severity}
color={item.severity}
size='small'
variant='tonal'
/>
</div>
<Typography variant='caption' color='text.secondary'>
Value: {item.value} (Expected: {item.expected}) · Deviation: {item.deviation}
</Typography>
</div>
))
)}
</CardContent>
</Card>
)
}
export default AnomalyDetectionCard
@@ -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 (
<Card>
<CardHeader
title='Economic Overview'
subheader='Costs & ROI'
action={<OptionMenu options={['Export PDF', 'Export Excel', 'Details']} />}
/>
<CardContent className='flex flex-col gap-4'>
<Grid container spacing={4}>
{economicData.map((item, index) => (
<Grid size={{ xs: 12, sm: 6 }} key={index}>
<div className='flex items-center gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={40}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} />
</CustomAvatar>
<div>
<Typography variant='h6'>{item.value}</Typography>
<Typography variant='body2' color='text.secondary'>
{item.title}
</Typography>
<Typography variant='caption' color='text.disabled'>
{item.subtitle}
</Typography>
</div>
</div>
</Grid>
))}
</Grid>
<AppReactApexCharts type='bar' height={180} width='100%' series={series} options={options} />
</CardContent>
</Card>
)
}
export default EconomicOverview
@@ -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)<TimelineProps>({
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 (
<Card>
<CardHeader
avatar={<i className='tabler-bell-ring text-xl' />}
title='AI Alerts'
titleTypographyProps={{ variant: 'h5' }}
subheader='Explainable recommendations'
action={<OptionMenu options={['View All', 'Configure', 'Export']} />}
sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }}
/>
<CardContent className='flex flex-col gap-6 pbe-5'>
<Timeline>
{alerts.map((alert, index) => (
<TimelineItem key={index}>
<TimelineSeparator>
<TimelineDot color={alert.color} />
{index < alerts.length - 1 && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent>
<div className='flex flex-wrap items-center justify-between gap-x-2 mbe-2.5'>
<Typography className='font-medium' color='text.primary'>
{alert.title}
</Typography>
<Typography variant='caption'>{alert.time}</Typography>
</div>
<Typography variant='body2' color='text.secondary'>
{alert.description}
</Typography>
</TimelineContent>
</TimelineItem>
))}
</Timeline>
</CardContent>
</Card>
)
}
export default FarmAlertsTimeline
@@ -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 (
<Card>
<CardHeader
title='Active Alerts'
subheader='Requires Attention'
action={<OptionMenu options={['View All', 'Dismiss', 'Settings']} />}
/>
<CardContent className='flex flex-col sm:flex-row items-center justify-between gap-7'>
<div className='flex flex-col gap-6 is-full sm:is-[unset]'>
<div className='flex flex-col'>
<Typography variant='h2'>3</Typography>
<Typography>Total Alerts</Typography>
</div>
<div className='flex flex-col gap-4 is-full'>
{data.map((item, index) => (
<div key={index} className='flex items-center gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={34}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} />
</CustomAvatar>
<div className='flex flex-col'>
<Typography className='font-medium' color='text.primary'>
{item.title}
</Typography>
<Typography variant='body2'>{item.subtitle}</Typography>
</div>
</div>
))}
</div>
</div>
<AppReactApexCharts type='radialBar' height={350} width='100%' series={[30]} options={options} />
</CardContent>
</Card>
)
}
export default FarmAlertsTracker
@@ -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<RowId, CardId[]>
type RowLabelsType = Record<RowId, string>
type CardLabelsType = Record<CardId, string>
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 (
<Drawer
open={open}
onClose={onClose}
anchor='end'
variant='temporary'
ModalProps={{
disablePortal: true,
disableAutoFocus: true,
disableScrollLock: true,
keepMounted: true
}}
PaperProps={{
className: 'is-[320px] shadow-none'
}}
sx={{ zIndex: 'drawer' }}
>
<Box className='flex flex-col is-full' sx={{ height: '100%' }}>
<Box className='p-6'>
<Typography variant='h5'>Dashboard Settings</Typography>
<Typography variant='body2' color='text.secondary' sx={{ mt: 0.5 }}>
Toggle cards to show or hide on the dashboard
</Typography>
</Box>
<Divider />
<PerfectScrollbar options={{ wheelPropagation: false }} className='flex-1'>
<List className='p-4'>
{ROW_IDS.map(rowId => (
<Box key={rowId}>
<Typography variant='subtitle2' color='text.secondary' sx={{ px: 2, py: 1 }}>
{rowLabels[rowId]}
</Typography>
{rowCards[rowId].map(cardId => {
const isDisabled = disabledSet.has(cardId)
return (
<ListItem key={cardId} disablePadding sx={{ py: 0.5 }}>
<FormControlLabel
control={
<Checkbox
checked={!isDisabled}
onChange={e => onToggleCard(cardId, !e.target.checked)}
size='small'
/>
}
label={<ListItemText primary={cardLabels[cardId]} primaryTypographyProps={{ variant: 'body2' }} />}
sx={{ m: 0, width: '100%' }}
/>
</ListItem>
)
})}
<Divider component='li' sx={{ my: 2 }} />
</Box>
))}
</List>
</PerfectScrollbar>
</Box>
</Drawer>
)
}
export default FarmDashboardSettingsDrawer
@@ -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<RowId, CardId[]>
type RowLabelsType = Record<RowId, string>
type CardLabelsType = Record<CardId, string>
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<HTMLButtonElement>(null)
const disabledSet = new Set(disabledCardIds)
const handleClose = () => setOpen(false)
const handleToggle = () => setOpen(prev => !prev)
return (
<>
<Box display='flex' alignItems='center' gap={0.5}>
{saving && <CircularProgress size={20} />}
<IconButton
ref={anchorRef}
onClick={handleToggle}
aria-label='Dashboard settings'
size='small'
color='default'
>
<i className='tabler-settings text-xl' />
</IconButton>
</Box>
<Popper
open={open}
transition
disablePortal
placement='bottom-start'
anchorEl={anchorRef.current}
className='min-is-[300px] max-is-[360px] !mbs-3 z-[1400]'
>
{({ TransitionProps, placement }) => (
<Fade
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom-start' ? 'left top' : 'right top' }}
>
<Paper className={settings.skin === 'bordered' ? 'border shadow-none' : 'shadow-lg'}>
<ClickAwayListener onClickAway={handleClose}>
<Box className='flex flex-col max-bs-[70vh]'>
<Box className='p-4'>
<Typography variant='h6'>Dashboard Settings</Typography>
<Typography variant='body2' color='text.secondary' sx={{ mt: 0.5 }}>
Toggle cards to show or hide
</Typography>
</Box>
<Box className='px-4 pb-2'>
<FormControlLabel
control={
<Switch
checked={enableDragReorder}
onChange={e => onToggleDragReorder(e.target.checked)}
size='small'
/>
}
label={
<Typography variant='body2'>Enable drag & reorder rows</Typography>
}
sx={{ m: 0 }}
/>
</Box>
<Divider />
<List
className='overflow-y-auto flex-1 px-4 pb-4'
sx={{ maxHeight: 'calc(70vh - 100px)' }}
>
{ROW_IDS.map(rowId => (
<Box key={rowId}>
<Typography variant='caption' color='text.secondary' sx={{ px: 0, py: 1, display: 'block' }}>
{rowLabels[rowId]}
</Typography>
{rowCards[rowId].map(cardId => {
const isDisabled = disabledSet.has(cardId)
return (
<ListItem key={cardId} disablePadding sx={{ py: 0.25 }}>
<FormControlLabel
control={
<Checkbox
checked={!isDisabled}
onChange={e => onToggleCard(cardId, !e.target.checked)}
size='small'
/>
}
label={
<ListItemText
primary={cardLabels[cardId]}
primaryTypographyProps={{ variant: 'body2' }}
/>
}
sx={{ m: 0, width: '100%' }}
/>
</ListItem>
)
})}
</Box>
))}
</List>
</Box>
</ClickAwayListener>
</Paper>
</Fade>
)}
</Popper>
</>
)
}
export default FarmDashboardSettingsDropdown
@@ -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<CardId, React.ComponentType> = {
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<FarmDashboardConfig>(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(
<FarmDashboardSettingsDropdown
disabledCardIds={config.disabledCardIds}
onToggleCard={handleToggleCard}
enableDragReorder={config.enableDragReorder ?? true}
onToggleDragReorder={handleToggleDragReorder}
cardLabels={CARD_LABELS}
rowLabels={ROW_LABELS}
rowCards={ROW_CARDS}
saving={saving}
/>
)
return () => setSlotContent(null)
}, [setSlotContent, config.disabledCardIds, config.enableDragReorder, handleToggleCard, handleToggleDragReorder, saving])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
return (
<Box position='relative'>
<Grid container spacing={6} ref={containerRef as RefObject<HTMLDivElement>}>
{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 (
<Grid
key={rowId}
size={12}
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
...(config.enableDragReorder !== false && { '&:hover .row-drag-handle': { opacity: 1 } })
}}
>
{config.enableDragReorder !== false && (
<IconButton
className='row-drag-handle'
size='small'
sx={{
opacity: 0.5,
cursor: 'grab',
flexShrink: 0,
mt: 1,
'&:active': { cursor: 'grabbing' }
}}
aria-label={`Drag ${ROW_LABELS[rowId as RowId]}`}
>
<i className='tabler-arrows-move text-textSecondary' />
</IconButton>
)}
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
{isOverviewRow && cards.includes('farmOverviewKpis') && <FarmOverviewKPIs />}
{!isOverviewRow &&
cards.map((cardId: CardId) => {
const size = CARD_GRID_SIZE[cardId]
const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={size} sx={cardRowSx}>
<Component />
</Grid>
)
})}
</Grid>
</Grid>
)
})}
</Grid>
</Box>
)
}
export default FarmDashboardWrapper
@@ -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 (
<>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Farm Health Score'
subtitle='AI Analysis'
stats='87%'
avatarColor='success'
avatarIcon='tabler-heartbeat'
avatarSkin='light'
avatarSize={44}
chipText='Good'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Water Stress Index'
subtitle='Current'
stats='12%'
avatarColor='info'
avatarIcon='tabler-droplet'
avatarSkin='light'
avatarSize={44}
chipText='Low'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Disease Risk'
subtitle='Last 7 Days'
stats='Low'
avatarColor='success'
avatarIcon='tabler-bug'
avatarSkin='light'
avatarSize={44}
chipText='5%'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Avg Soil Moisture'
subtitle='Field-wide'
stats='65%'
avatarColor='primary'
avatarIcon='tabler-plant-2'
avatarSkin='light'
avatarSize={44}
chipText='Optimal'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Yield Prediction'
subtitle='This Season'
stats='42 ton'
avatarColor='secondary'
avatarIcon='tabler-chart-bar'
avatarSkin='light'
avatarSize={44}
chipText='+8%'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Pest Risk'
subtitle='AI Forecast'
stats='15%'
avatarColor='warning'
avatarIcon='tabler-bug-off'
avatarSkin='light'
avatarSize={44}
chipText='Monitor'
chipColor='warning'
chipVariant='tonal'
/>
</Grid>
</>
)
}
export default FarmOverviewKPIs
@@ -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 (
<Card className='pbe-6'>
<CardHeader
title='Weather Today'
subheader='Clear, 24°C'
className='pbe-3'
action={<OptionMenu options={['Refresh', '7-day forecast', 'Details']} />}
/>
<CardContent>
<div className='flex items-center gap-2 mbe-2'>
<i className='tabler-sun text-4xl text-warning' />
<Typography variant='h4'>24°C</Typography>
<Typography color='text.disabled' variant='body2'>
Humid: 45% | Wind: 12 km/h
</Typography>
</div>
</CardContent>
<AppReactApexCharts type='area' height={88} width='100%' series={series} options={options} />
</Card>
)
}
export default FarmWeatherCard
@@ -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 (
<Card>
<CardHeader
avatar={
<CustomAvatar skin='light' color='success' size={44}>
<i className='tabler-calendar-event text-2xl' />
</CustomAvatar>
}
title='Harvest Prediction'
subheader='AI Estimated Date'
action={<OptionMenu options={['Details', 'Adjust', 'Export']} />}
/>
<CardContent className='flex flex-col gap-4'>
<div className='flex items-center gap-4'>
<Typography variant='h3'>Oct 15, 2025</Typography>
<Chip label='58 days' color='info' size='small' variant='tonal' />
</div>
<Typography variant='body2' color='text.secondary'>
Based on current GDD accumulation and weather forecast. Optimal harvest window: Oct 12-18.
</Typography>
</CardContent>
</Card>
)
}
export default HarvestPredictionCard
@@ -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 (
<Card>
<CardHeader
avatar={<i className='tabler-chart-radar text-xl' />}
title='NDVI Health'
subheader='Vegetation Index'
sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }}
/>
<CardContent className='flex flex-col sm:flex-row items-center justify-between gap-6'>
<div className='flex flex-col gap-4 is-full sm:is-[unset]'>
<Typography variant='h2'>0.78</Typography>
<Typography variant='body2'>NDVI Index (0-1)</Typography>
<div className='flex flex-col gap-3'>
{healthData.map((item, index) => (
<div key={index} className='flex items-center gap-3'>
<CustomAvatar skin='light' variant='rounded' color={item.color as 'success'} size={32}>
<i className={classnames(item.icon, 'text-lg')} />
</CustomAvatar>
<div>
<Typography variant='body2'>{item.title}</Typography>
<Chip label={item.value} color={item.color as 'success'} size='small' variant='tonal' />
</div>
</div>
))}
</div>
</div>
<AppReactApexCharts type='radialBar' height={200} width='100%' series={[78]} options={options} />
</CardContent>
</Card>
)
}
export default NDVIHealthCard
@@ -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 (
<Card>
<CardHeader
title='AI Recommendations'
subheader='Action Items'
action={<OptionMenu options={['Export', 'Snooze', 'Mark Done']} />}
/>
<CardContent className='flex flex-col gap-4'>
{data.map((item, index) => (
<div key={index} className='flex items-start gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={38}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} />
</CustomAvatar>
<div className='flex flex-col flex-1 min-w-0'>
<Typography className='font-medium' color='text.primary'>
{item.title}
</Typography>
<Typography variant='body2' color='text.secondary'>
{item.subtitle}
</Typography>
</div>
</div>
))}
</CardContent>
</Card>
)
}
export default RecommendationsList
@@ -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 (
<Card>
<CardHeader
title='Soil Moisture Trend'
subheader='Today vs Last Week'
/>
<CardContent>
<div className='flex items-center gap-4 mbe-4'>
<Typography variant='h4'>48%</Typography>
<Typography color='success.main' variant='body2'>
+5% vs last week
</Typography>
</div>
<AppReactApexCharts type='area' height={280} width='100%' series={series} options={options} />
</CardContent>
</Card>
)
}
export default SensorComparisonChart
@@ -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 (
<Card>
<CardHeader
title='Sensor Comparison'
subheader='Today vs Ideal Ranges'
action={<OptionMenu options={['Today', 'This Week', 'This Month']} />}
/>
<CardContent>
<AppReactApexCharts type='radar' height={373} width='100%' series={series} options={options} />
</CardContent>
</Card>
)
}
export default SensorRadarChart
@@ -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 (
<Card>
<CardHeader
title='Environmental Sensors'
subheader='Real-time Data'
action={<OptionMenu options={['Last Hour', 'Last 24h', 'Last 7 Days']} />}
/>
<CardContent className='flex flex-col gap-4'>
{data.map((item, index) => (
<div key={index} className='flex items-center gap-4'>
<div className='flex flex-wrap justify-between items-center gap-x-4 gap-y-1 is-full'>
<div className='flex flex-col'>
<Typography className='font-medium' color='text.primary'>
{item.title}
</Typography>
<Typography variant='body2'>{item.subtitle}</Typography>
</div>
<div className='flex items-center gap-1'>
<i
className={classnames(
item.trend === 'negative' ? 'tabler-chevron-down text-error' : 'tabler-chevron-up text-success',
'text-xl'
)}
/>
<Typography
variant='h6'
color={`${item.trend === 'negative' ? 'error' : 'success'}.main`}
>{`${item.trendNumber > 0 ? '+' : ''}${item.trendNumber}%`}</Typography>
</div>
</div>
</div>
))}
</CardContent>
</Card>
)
}
export default SensorValuesList
@@ -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 (
<Card>
<CardHeader
title='Soil Moisture Heatmap'
subheader='Field Zones by Time'
/>
<CardContent>
<AppReactApexCharts type='heatmap' width='100%' height={350} options={options} series={series} />
</CardContent>
</Card>
)
}
export default SoilMoistureHeatmap
@@ -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}`
}
},
tooltip: {
y: { formatter: (val: number) => `${val}` }
}
}
return (
<Card>
<CardHeader
title='7-Day Water Need Prediction'
subheader='AI Forecast'
action={<OptionMenu options={['Export', 'Adjust', 'Details']} />}
/>
<CardContent>
<div className='flex items-center gap-4 mbe-4'>
<Typography variant='h4'>3,290 m³</Typography>
<Typography variant='body2' color='text.secondary'>
Total next 7 days
</Typography>
</div>
<AppReactApexCharts type='area' height={250} width='100%' series={series} options={options} />
</CardContent>
</Card>
)
}
export default WaterNeedPrediction
@@ -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 (
<Card>
<CardHeader
title='Yield Prediction'
subheader='This Year vs Last Year'
action={<OptionMenu options={['Export', 'Compare', 'Details']} />}
/>
<CardContent className='flex flex-col gap-4'>
<AppReactApexCharts type='line' height={280} width='100%' series={series} options={options} />
<div className='flex flex-col gap-4'>
{summaryData.map((item, index) => (
<div key={index} className='flex items-center gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={38}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} />
</CustomAvatar>
<div className='flex justify-between items-center is-full'>
<div className='flex flex-col'>
<Typography className='font-medium' color='text.primary'>
{item.title}
</Typography>
<Typography variant='body2'>{item.subtitle}</Typography>
</div>
<Typography className='font-medium' color='success.main'>
{item.amount}
</Typography>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}
export default YieldPredictionChart
@@ -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<CardId, RowId> = {
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<CardId, { xs?: number; sm?: number; md?: number; lg: number }> = {
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<CardId, string> = {
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<RowId, string> = {
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<RowId, CardId[]> = {
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
}