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,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