Remove deprecated dashboard pages and update vertical menu links for streamlined navigation

- Deleted unused pages for crop zoning, farm AI assistant, fertilization recommendation, irrigation recommendation, pest detection, plant simulator, soil data, and water data.
- Updated the vertical menu to reflect the removal of these pages, ensuring a cleaner and more efficient user experience.
This commit is contained in:
2026-02-21 22:05:47 +03:30
parent 00b5ecba7f
commit cb29828a69
16 changed files with 208 additions and 126 deletions
+14 -16
View File
@@ -95,36 +95,34 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
{t('dashboards')} {t('dashboards')}
</MenuItem> </MenuItem>
<MenuSection label={t('dataSection')}> <MenuSection label={t('dataSection')}>
<MenuItem href="/water-data" icon={<i className="tabler-droplet" />}>
<MenuItem href="/dashboard/water-data" icon={<i className="tabler-droplet" />}>
{t('waterData')} {t('waterData')}
</MenuItem> </MenuItem>
<MenuItem href="/dashboard/soil-data" icon={<i className="tabler-seedling" />}> <MenuItem href="/soil-data" icon={<i className="tabler-seedling" />}>
{t('soilData')} {t('soilData')}
</MenuItem> </MenuItem>
<MenuItem href="/dashboard/crop-zoning" icon={<i className="tabler-map-2" />}> <MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
{t('cropZoning')} {t('cropZoning')}
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
<MenuSection label={t('simulator')}> <MenuSection label={t('simulator')}>
<MenuItem href="/dashboard/plant-simulator" icon={<i className="tabler-flower" />}> <MenuItem href="/plant-simulator" icon={<i className="tabler-flower" />}>
{t('plantSimulator')} {t('plantSimulator')}
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
<MenuSection label={t('recommendation')}> <MenuSection label={t('recommendation')}>
<MenuItem href="/irrigation-recommendation" icon={<i className="tabler-droplet-half-2" />}>
<MenuItem href="/dashboard/irrigation-recommendation" icon={<i className="tabler-droplet-half-2" />}>
{t('irrigationRecommendation')} {t('irrigationRecommendation')}
</MenuItem> </MenuItem>
<MenuItem href="/dashboard/fertilization-recommendation" icon={<i className="tabler-atom-2" />}> <MenuItem href="/fertilization-recommendation" icon={<i className="tabler-atom-2" />}>
{t('fertilizationRecommendation')} {t('fertilizationRecommendation')}
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
<MenuSection label={t('aiAssistant')}> <MenuSection label={t('aiAssistant')}>
<MenuItem href="/dashboard/farm-ai-assistant" icon={<i className="tabler-robot" />}> <MenuItem href="/farm-ai-assistant" icon={<i className="tabler-robot" />}>
{t('farmAiAssistant')} {t('farmAiAssistant')}
</MenuItem> </MenuItem>
<MenuItem href="/dashboard/pest-detection" icon={<i className="tabler-bug" />}> <MenuItem href="/pest-detection" icon={<i className="tabler-bug" />}>
{t('pestDetection')} {t('pestDetection')}
</MenuItem> </MenuItem>
</MenuSection> </MenuSection>
@@ -18,19 +18,18 @@ import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard'
// Service // Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService' import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig' import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig'
const SOIL_CARD_IDS: CardId[] = [ /** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
'soilMoistureHeatmap', const SOIL_ROWS: CardId[][] = [
'sensorValuesList', ['soilMoistureHeatmap'],
'sensorRadarChart', ['sensorValuesList', 'sensorRadarChart'],
'sensorComparisonChart', ['sensorComparisonChart', 'anomalyDetectionCard']
'anomalyDetectionCard'
] ]
const cardRowSx = { const cardRowSx = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 } '& > *': { flex: 1, minHeight: 0 }
} }
@@ -65,18 +64,28 @@ const SoilDataDashboardWrapper = () => {
return ( return (
<Box position='relative'> <Box position='relative'>
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'flex-start' }}> {SOIL_ROWS.map((rowCards, rowIndex) => {
{SOIL_CARD_IDS.map(cardId => { const sizePerCard = 12 / rowCards.length
const size = CARD_GRID_SIZE[cardId] return (
const Component = CARD_COMPONENTS[cardId] <Grid
if (!Component) return null key={rowIndex}
return ( size={12}
<Grid key={cardId} size={size} sx={cardRowSx}> container
<Component data={cardsData?.[cardId]} /> spacing={6}
</Grid> sx={{ display: 'flex', alignItems: 'stretch' }}
) >
})} {rowCards.map(cardId => {
</Grid> const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
</Grid> </Grid>
</Box> </Box>
) )
@@ -17,18 +17,17 @@ import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
// Service // Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService' import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig' import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig'
const WATER_CARD_IDS: CardId[] = [ /** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
'farmWeatherCard', const WATER_ROWS: CardId[][] = [
'farmAlertsTimeline', ['farmWeatherCard', 'farmAlertsTimeline', 'waterNeedPrediction'],
'waterNeedPrediction', ['sensorValuesList']
'sensorValuesList'
] ]
const cardRowSx = { const cardRowSx = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 } '& > *': { flex: 1, minHeight: 0 }
} }
@@ -62,18 +61,28 @@ const WaterDataDashboardWrapper = () => {
return ( return (
<Box position='relative'> <Box position='relative'>
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'flex-start' }}> {WATER_ROWS.map((rowCards, rowIndex) => {
{WATER_CARD_IDS.map(cardId => { const sizePerCard = 12 / rowCards.length
const size = CARD_GRID_SIZE[cardId] return (
const Component = CARD_COMPONENTS[cardId] <Grid
if (!Component) return null key={rowIndex}
return ( size={12}
<Grid key={cardId} size={size} sx={cardRowSx}> container
<Component data={cardsData?.[cardId]} /> spacing={6}
</Grid> sx={{ display: 'flex', alignItems: 'stretch' }}
) >
})} {rowCards.map(cardId => {
</Grid> const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
</Grid> </Grid>
</Box> </Box>
) )
@@ -19,6 +19,10 @@ type CropZoningMapProps = {
onZoneClick?: (zone: ZoneFeatureProperties) => void onZoneClick?: (zone: ZoneFeatureProperties) => void
optimizationKey?: number optimizationKey?: number
className?: string className?: string
/** منطقهٔ اولیه از دیتای ماک؛ وقتی مقدار دارد نقشه فقط نمایشی است و کاربر نمیتواند منطقه را تغییر دهد */
initialAreaGeoJson?: MapDrawGeoJSON | null
/** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */
readOnly?: boolean
} }
export default function CropZoningMap({ export default function CropZoningMap({
@@ -29,7 +33,9 @@ export default function CropZoningMap({
onAreaChange, onAreaChange,
onZoneClick, onZoneClick,
optimizationKey = 0, optimizationKey = 0,
className = '' className = '',
initialAreaGeoJson = null,
readOnly = false
}: CropZoningMapProps) { }: CropZoningMapProps) {
const mapRef = useRef<HTMLDivElement>(null) const mapRef = useRef<HTMLDivElement>(null)
const mapInstanceRef = useRef<L.Map | null>(null) const mapInstanceRef = useRef<L.Map | null>(null)
@@ -124,21 +130,22 @@ export default function CropZoningMap({
const drawnItems = L.featureGroup().addTo(map) const drawnItems = L.featureGroup().addTo(map)
drawnItemsRef.current = drawnItems drawnItemsRef.current = drawnItems
const drawControl = new L.Control.Draw({ if (!readOnly) {
position: 'topright', const drawControl = new L.Control.Draw({
draw: { position: 'topright',
polygon: { shapeOptions: { color: '#3388ff' } }, draw: {
rectangle: { shapeOptions: { color: '#3388ff' } }, polygon: { shapeOptions: { color: '#3388ff' } },
circle: false, rectangle: { shapeOptions: { color: '#3388ff' } },
circlemarker: false, circle: false,
marker: false, circlemarker: false,
polyline: false marker: false,
}, polyline: false
edit: { featureGroup: drawnItems, remove: true } },
}) edit: { featureGroup: drawnItems, remove: true }
})
map.addControl(drawControl) map.addControl(drawControl)
drawControlRef.current = drawControl drawControlRef.current = drawControl
}
const getGeoJsonFromDrawn = (): MapDrawGeoJSON => { const getGeoJsonFromDrawn = (): MapDrawGeoJSON => {
const geojson = drawnItems.toGeoJSON() const geojson = drawnItems.toGeoJSON()
@@ -159,6 +166,12 @@ export default function CropZoningMap({
} }
} }
if (readOnly && initialAreaGeoJson && initialAreaGeoJson.geometry && (initialAreaGeoJson.geometry as { type: string }).type === 'Polygon') {
drawnItems.clearLayers()
L.geoJSON(initialAreaGeoJson as Feature<Polygon>).eachLayer((layer) => drawnItems.addLayer(layer))
emitAndRender()
}
const onCreated = (e: L.LeafletEvent) => { const onCreated = (e: L.LeafletEvent) => {
const event = e as L.DrawEvents.Created const event = e as L.DrawEvents.Created
drawnItems.clearLayers() drawnItems.clearLayers()
@@ -175,17 +188,23 @@ export default function CropZoningMap({
} }
} }
map.on(L.Draw.Event.CREATED, onCreated) if (!readOnly) {
map.on(L.Draw.Event.EDITED, onEdited) map.on(L.Draw.Event.CREATED, onCreated)
map.on(L.Draw.Event.DELETED, onDeleted) map.on(L.Draw.Event.EDITED, onEdited)
map.on(L.Draw.Event.DELETED, onDeleted)
}
mapInstanceRef.current = map mapInstanceRef.current = map
cleanupFn = () => { cleanupFn = () => {
map.off(L.Draw.Event.CREATED, onCreated) if (!readOnly) {
map.off(L.Draw.Event.EDITED, onEdited) map.off(L.Draw.Event.CREATED, onCreated)
map.off(L.Draw.Event.DELETED, onDeleted) map.off(L.Draw.Event.EDITED, onEdited)
map.removeControl(drawControl) map.off(L.Draw.Event.DELETED, onDeleted)
if (drawControlRef.current) {
map.removeControl(drawControlRef.current)
}
}
if (zonesLayerRef.current) map.removeLayer(zonesLayerRef.current) if (zonesLayerRef.current) map.removeLayer(zonesLayerRef.current)
map.remove() map.remove()
mapInstanceRef.current = null mapInstanceRef.current = null
@@ -96,64 +96,77 @@ export default function CropZoningWeatherSection() {
const humidity = (weatherData.humidity as number) ?? 45 const humidity = (weatherData.humidity as number) ?? 45
const windSpeed = (weatherData.windSpeed as number) ?? 12 const windSpeed = (weatherData.windSpeed as number) ?? 12
const cardRowSx = {
display: 'flex',
flexDirection: 'column' as const,
minHeight: 200,
'& > *': { flex: 1, minHeight: 0 }
}
return ( return (
<Box className='pis-0 pie-0'> <Box className='pis-0 pie-0'>
<Typography variant='h6' className='mbe-4 font-semibold'> <Typography variant='h6' className='mbe-4 font-semibold'>
{t('title')} {t('title')}
</Typography> </Typography>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6, lg: 4 }}> {/* Row 1: Weather + 3 KPI cards — equal width (3 each on md+) */}
<FarmWeatherCard data={weatherData} /> <Grid
size={12}
container
spacing={3}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
<Grid size={{ xs: 12, md: 3 }} sx={cardRowSx}>
<FarmWeatherCard data={weatherData} />
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-temperature text-3xl text-error' />
<Typography variant='body2' color='text.secondary'>
{t('temperature')}
</Typography>
<Typography variant='h5'>
{temp}
{weatherData.unit ?? '°C'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-droplet text-3xl text-info' />
<Typography variant='body2' color='text.secondary'>
{t('humidity')}
</Typography>
<Typography variant='h5'>{humidity}%</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 3 }} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent className='flex flex-1 flex-col items-center justify-center gap-2'>
<i className='tabler-wind text-3xl text-success' />
<Typography variant='body2' color='text.secondary'>
{t('windSpeed')}
</Typography>
<Typography variant='h5'>
{windSpeed} {(weatherData.windUnit as string) ?? 'km/h'}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}> {/* Row 2: Forecast chart — full width */}
<Card> <Grid size={12} sx={cardRowSx}>
<CardContent className='flex flex-col items-center gap-2'> <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 280 }}>
<i className='tabler-temperature text-3xl text-error' />
<Typography variant='body2' color='text.secondary'>
{t('temperature')}
</Typography>
<Typography variant='h5'>
{temp}
{weatherData.unit ?? '°C'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<Card>
<CardContent className='flex flex-col items-center gap-2'>
<i className='tabler-droplet text-3xl text-info' />
<Typography variant='body2' color='text.secondary'>
{t('humidity')}
</Typography>
<Typography variant='h5'>{humidity}%</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<Card>
<CardContent className='flex flex-col items-center gap-2'>
<i className='tabler-wind text-3xl text-success' />
<Typography variant='body2' color='text.secondary'>
{t('windSpeed')}
</Typography>
<Typography variant='h5'>
{windSpeed} {(weatherData.windUnit as string) ?? 'km/h'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12 }}>
<Card>
<CardHeader <CardHeader
title={t('forecastChart')} title={t('forecastChart')}
subheader={t('forecastSubheader')} subheader={t('forecastSubheader')}
/> />
<CardContent> <CardContent sx={{ flex: 1, minHeight: 0 }}>
<AppReactApexCharts <AppReactApexCharts
type='line' type='line'
height={260} height={260}
@@ -11,6 +11,7 @@ import ZoneLegend from './ZoneLegend'
import LayerControl from './LayerControl' import LayerControl from './LayerControl'
import ZoneDetailPanel from './ZoneDetailPanel' import ZoneDetailPanel from './ZoneDetailPanel'
import CropZoningWeatherSection from './CropZoningWeatherSection' import CropZoningWeatherSection from './CropZoningWeatherSection'
import { MOCK_AREA_GEOJSON } from './cropZoningMockData'
import type { LayerType } from './cropZoningTypes' import type { LayerType } from './cropZoningTypes'
import type { ZoneFeatureProperties } from './cropZoningTypes' import type { ZoneFeatureProperties } from './cropZoningTypes'
import type { MapDrawGeoJSON } from './CropZoningMap' import type { MapDrawGeoJSON } from './CropZoningMap'
@@ -26,7 +27,7 @@ const MapComponent = dynamic(() => Promise.resolve(CropZoningMap), {
export default function CropZoningWrapper() { export default function CropZoningWrapper() {
const t = useTranslations('cropZoning') const t = useTranslations('cropZoning')
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null) const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(MOCK_AREA_GEOJSON)
const [activeLayer, setActiveLayer] = useState<LayerType>('crops') const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
const [selectedZone, setSelectedZone] = useState<ZoneFeatureProperties | null>(null) const [selectedZone, setSelectedZone] = useState<ZoneFeatureProperties | null>(null)
const [panelOpen, setPanelOpen] = useState(false) const [panelOpen, setPanelOpen] = useState(false)
@@ -58,6 +59,8 @@ export default function CropZoningWrapper() {
onZoneClick={handleZoneClick} onZoneClick={handleZoneClick}
optimizationKey={optimizationKey} optimizationKey={optimizationKey}
className='min-bs-[400px]' className='min-bs-[400px]'
initialAreaGeoJson={MOCK_AREA_GEOJSON}
readOnly
/> />
</Box> </Box>
@@ -0,0 +1,23 @@
import type { MapDrawGeoJSON } from './CropZoningMap'
/**
* منطقهٔ ثابت برای نمایش روی نقشه (دیتای ماک).
* کاربر امکان تغییر یا رسم منطقهٔ جدید را ندارد.
* مختصات: یک چندضلعی حول تهران [35.6892, 51.389] — در GeoJSON به صورت [lng, lat].
*/
export const MOCK_AREA_GEOJSON: MapDrawGeoJSON = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[51.38, 35.68],
[51.40, 35.68],
[51.40, 35.70],
[51.38, 35.70],
[51.38, 35.68]
]
]
}
} as MapDrawGeoJSON
@@ -415,6 +415,7 @@ const GrowthChart = memo(function GrowthChart({
const chartOptions = { const chartOptions = {
responsive: true, responsive: true,
maintainAspectRatio: false,
animation: { duration: 0 }, animation: { duration: 0 },
plugins: { plugins: {
legend: { labels: { font: { size: 11 } } }, legend: { labels: { font: { size: 11 } } },
@@ -510,7 +511,14 @@ const GrowthChart = memo(function GrowthChart({
] ]
} }
return <Line data={data} options={chartOptions} /> return (
<Box
className='is-full'
sx={{ minHeight: { xs: 380, sm: 340, md: 320 }, height: { xs: 380, sm: 340, md: 320 } }}
>
<Line data={data} options={chartOptions} />
</Box>
)
}) })
// ─── Main Component ─────────────────────────────────────────────────────────── // ─── Main Component ───────────────────────────────────────────────────────────
@@ -715,12 +723,12 @@ export default function PlantSimulator() {
] ]
return ( return (
<Box className='flex flex-col gap-6 is-full'> <Box className='flex flex-col gap-6 min-is-0 is-full'>
<Typography variant='h4' className='text-center font-bold'> <Typography variant='h4' className='text-center font-bold'>
🌱 {t('title')} 🌱 {t('title')}
</Typography> </Typography>
<Grid container spacing={6} className='max-w-6xl mx-auto'> <Grid container spacing={6} className='min-is-0 is-full'>
{/* ── Left: Plant visualization ── */} {/* ── Left: Plant visualization ── */}
<Grid size={{ xs: 12, lg: 4 }} className='flex flex-col items-center gap-4'> <Grid size={{ xs: 12, lg: 4 }} className='flex flex-col items-center gap-4'>
<Card className='is-full flex flex-col items-center p-6'> <Card className='is-full flex flex-col items-center p-6'>