310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
'use client'
|
|
|
|
// React Imports
|
|
import type { RefObject } from 'react'
|
|
import { useEffect, useMemo, useState, useCallback, useContext } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
|
|
// 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,
|
|
CARD_GRID_SIZE,
|
|
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 t = useTranslations('farmDashboard')
|
|
const { setSlotContent } = useContext(NavbarSlotContext)
|
|
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
|
|
|
|
const cardLabels = useMemo(
|
|
() =>
|
|
Object.fromEntries(
|
|
(
|
|
[
|
|
'farmOverviewKpis',
|
|
'farmWeatherCard',
|
|
'farmAlertsTracker',
|
|
'sensorValuesList',
|
|
'sensorRadarChart',
|
|
'sensorComparisonChart',
|
|
'anomalyDetectionCard',
|
|
'farmAlertsTimeline',
|
|
'waterNeedPrediction',
|
|
'harvestPredictionCard',
|
|
'yieldPredictionChart',
|
|
'soilMoistureHeatmap',
|
|
'ndviHealthCard',
|
|
'recommendationsList',
|
|
'economicOverview'
|
|
] as CardId[]
|
|
).map((id) => [id, t(`cards.${id}`)])
|
|
) as Record<CardId, string>,
|
|
[t]
|
|
)
|
|
|
|
const rowLabels = useMemo(
|
|
() =>
|
|
Object.fromEntries(
|
|
(
|
|
[
|
|
'overviewKpis',
|
|
'weatherAlerts',
|
|
'sensorMonitoring',
|
|
'sensorCharts',
|
|
'alertsWater',
|
|
'predictions',
|
|
'soilHeatmap',
|
|
'ndviRecommendations',
|
|
'economic'
|
|
] as RowId[]
|
|
).map((id) => [id, t(`rows.${id}`)])
|
|
) as Record<RowId, string>,
|
|
[t]
|
|
)
|
|
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={cardLabels}
|
|
rowLabels={rowLabels}
|
|
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={t('settings.dragRow', { row: rowLabels[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
|