Files
Frontend/src/views/dashboards/farm/FarmDashboardWrapper.tsx
T

264 lines
9.2 KiB
TypeScript
Raw Normal View History

'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 [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const disabledSet = new Set(config.disabledCardIds)
const hasVisibleCard = useCallback(
(rowId: string) => {
const cards = ROW_CARDS[rowId as RowId]
if (!Array.isArray(cards)) return false
return cards.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(() => {
Promise.all([farmDashboardService.getConfig(), farmDashboardService.getAllCards()])
.then(([configData, cards]) => {
const validRowOrder = (configData.rowOrder ?? []).filter(
(id): id is RowId => id in ROW_CARDS
)
const merged: FarmDashboardConfig = {
disabledCardIds: configData.disabledCardIds ?? [],
rowOrder: validRowOrder.length ? validRowOrder : [...ROW_IDS],
enableDragReorder: configData.enableDragReorder ?? true
}
setConfig(merged)
setCardsData(cards ?? {})
})
.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 data={cardsData?.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 data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
</Grid>
)
})}
</Grid>
</Box>
)
}
export default FarmDashboardWrapper