This commit is contained in:
2026-03-26 15:25:17 +03:30
parent 4b291155d0
commit e89c3a1b16
7 changed files with 425 additions and 249 deletions
+197 -151
View File
@@ -1,39 +1,39 @@
'use client'
"use client";
// React Imports
import type { RefObject } from 'react'
import { useEffect, useMemo, useState, useCallback, useContext } from 'react'
import { useTranslations } from 'next-intl'
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'
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'
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'
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'
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 {
@@ -43,18 +43,21 @@ import {
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'
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 }
}
display: "flex",
flexDirection: "column",
"& > *": { flex: 1, minHeight: 0 },
};
const CARD_COMPONENTS: Record<CardId, React.ComponentType<{ data?: Record<string, unknown> }>> = {
const CARD_COMPONENTS: Record<
CardId,
React.ComponentType<{ data?: Record<string, unknown> }>
> = {
farmOverviewKpis: FarmOverviewKPIs,
farmWeatherCard: FarmWeatherCard,
farmAlertsTracker: FarmAlertsTracker,
@@ -69,158 +72,179 @@ const CARD_COMPONENTS: Record<CardId, React.ComponentType<{ data?: Record<string
soilMoistureHeatmap: SoilMoistureHeatmap,
ndviHealthCard: NDVIHealthCard,
recommendationsList: RecommendationsList,
economicOverview: EconomicOverview
}
economicOverview: EconomicOverview,
};
function mergeRowOrderAfterDrag(
currentRowOrder: string[],
newVisibleOrder: string[],
visibleRows: string[]
visibleRows: string[],
): string[] {
const result = [...currentRowOrder]
let visibleIndex = 0
const result = [...currentRowOrder];
let visibleIndex = 0;
for (let i = 0; i < result.length; i++) {
if (visibleRows.includes(result[i])) {
result[i] = newVisibleOrder[visibleIndex++]
result[i] = newVisibleOrder[visibleIndex++];
}
}
return result
return result;
}
const FarmDashboardWrapper = () => {
const t = useTranslations('farmDashboard')
const { setSlotContent } = useContext(NavbarSlotContext)
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
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'
"farmOverviewKpis",
"farmWeatherCard",
"farmAlertsTracker",
"sensorValuesList",
"sensorRadarChart",
"sensorComparisonChart",
"anomalyDetectionCard",
"farmAlertsTimeline",
"waterNeedPrediction",
"harvestPredictionCard",
"yieldPredictionChart",
"soilMoistureHeatmap",
"ndviHealthCard",
"recommendationsList",
"economicOverview",
] as CardId[]
).map((id) => [id, t(`cards.${id}`)])
).map((id) => [id, t(`cards.${id}`)]),
) as Record<CardId, string>,
[t]
)
[t],
);
const rowLabels = useMemo(
() =>
Object.fromEntries(
(
[
'overviewKpis',
'weatherAlerts',
'sensorMonitoring',
'sensorCharts',
'alertsWater',
'predictions',
'soilHeatmap',
'ndviRecommendations',
'economic'
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic",
] as RowId[]
).map((id) => [id, t(`rows.${id}`)])
).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)
[t],
);
const disabledSet = new Set(config.disabledCardIds)
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))
const cards = ROW_CARDS[rowId as RowId];
if (!Array.isArray(cards)) return false;
return cards.some((cardId) => !disabledSet.has(cardId));
},
[config.disabledCardIds]
)
[config.disabledCardIds],
);
const visibleRowOrder = config.rowOrder.filter(hasVisibleCard)
const visibleRowOrder = config.rowOrder.filter(hasVisibleCard);
const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(
visibleRowOrder,
{
plugins: [animations()],
dragHandle: ".row-drag-handle",
},
);
// useEffect(()=>{
// console.log("ksjf",visibleRowOrder,orderedRows)
// },[visibleRowOrder,visibleRowOrder])
const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(visibleRowOrder, {
plugins: [animations()],
dragHandle: '.row-drag-handle'
})
useEffect(() => {
Promise.all([farmDashboardService.getConfig(), farmDashboardService.getAllCards()])
Promise.all([
farmDashboardService.getConfig(),
farmDashboardService.getAllCards(),
])
.then(([configData, cards]) => {
const validRowOrder = (configData.rowOrder ?? []).filter(
(id): id is RowId => id in ROW_CARDS
)
(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 ?? {})
enableDragReorder: configData.enableDragReorder ?? true,
};
setConfig(merged);
setCardsData(cards ?? {});
})
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
.finally(() => setLoading(false))
}, [])
.finally(() => setLoading(false));
}, []);
useEffect(() => {
setOrderedRows(visibleRowOrder)
setOrderedRows(visibleRowOrder);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.disabledCardIds])
}, [visibleRowOrder]);
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)
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))
.then((updated) => setConfig(updated))
.catch(() => {})
.finally(() => setSaving(false))
.finally(() => setSaving(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [orderedRows])
}, [orderedRows]);
const handleToggleDragReorder = useCallback((enabled: boolean) => {
setConfig(prev => ({ ...prev, enableDragReorder: enabled }))
setSaving(true)
setConfig((prev) => ({ ...prev, enableDragReorder: enabled }));
setSaving(true);
farmDashboardService
.updateConfig({ enableDragReorder: enabled })
.then(updated => setConfig(updated))
.finally(() => setSaving(false))
}, [])
.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)
: 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))
.then((updated) => setConfig(updated))
.catch(() => setConfig((prev) => ({ ...prev, disabledCardIds: next })))
.finally(() => setSaving(false));
},
[config.disabledCardIds]
)
[config.disabledCardIds],
);
useEffect(() => {
setSlotContent(
@@ -233,77 +257,99 @@ const FarmDashboardWrapper = () => {
rowLabels={rowLabels}
rowCards={ROW_CARDS}
saving={saving}
/>
)
return () => setSlotContent(null)
}, [setSlotContent, config.disabledCardIds, config.enableDragReorder, handleToggleCard, handleToggleDragReorder, saving])
/>,
);
return () => setSlotContent(null);
}, [
setSlotContent,
config.disabledCardIds,
config.enableDragReorder,
handleToggleCard,
handleToggleDragReorder,
saving,
]);
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={200}
>
<CircularProgress />
</Box>
)
);
}
return (
<Box position='relative'>
<Grid container spacing={6} ref={containerRef as RefObject<HTMLDivElement>}>
<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 cards = ROW_CARDS[rowId as RowId].filter(
(cardId) => !disabledSet.has(cardId),
);
if (cards.length === 0) return null;
const isOverviewRow = rowId === 'overviewKpis'
const isOverviewRow = rowId === "overviewKpis";
return (
<Grid
key={rowId}
size={12}
sx={{
display: 'flex',
alignItems: 'flex-start',
display: "flex",
alignItems: "flex-start",
gap: 2,
...(config.enableDragReorder !== false && { '&:hover .row-drag-handle': { opacity: 1 } })
...(config.enableDragReorder !== false && {
"&:hover .row-drag-handle": { opacity: 1 },
}),
}}
>
{config.enableDragReorder !== false && (
<IconButton
className='row-drag-handle'
size='small'
className="row-drag-handle"
size="small"
sx={{
opacity: 0.5,
cursor: 'grab',
cursor: "grab",
flexShrink: 0,
mt: 1,
'&:active': { cursor: 'grabbing' }
"&:active": { cursor: "grabbing" },
}}
aria-label={t('settings.dragRow', { row: rowLabels[rowId as RowId] })}
aria-label={t("settings.dragRow", {
row: rowLabels[rowId as RowId],
})}
>
<i className='tabler-arrows-move text-textSecondary' />
<i className="tabler-arrows-move text-textSecondary" />
</IconButton>
)}
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
{isOverviewRow && cards.includes('farmOverviewKpis') && (
{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
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
export default FarmDashboardWrapper;
@@ -25,6 +25,7 @@ const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => {
const kpis = (data?.kpis as KpiItem[] | undefined) ?? []
if (kpis.length === 0) return null
return (
<>
{kpis.map((kpi) => (