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:
@@ -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
|
||||
Reference in New Issue
Block a user