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
@@ -18,19 +18,18 @@ import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard'
// Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig'
const SOIL_CARD_IDS: CardId[] = [
'soilMoistureHeatmap',
'sensorValuesList',
'sensorRadarChart',
'sensorComparisonChart',
'anomalyDetectionCard'
/** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
const SOIL_ROWS: CardId[][] = [
['soilMoistureHeatmap'],
['sensorValuesList', 'sensorRadarChart'],
['sensorComparisonChart', 'anomalyDetectionCard']
]
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
@@ -65,18 +64,28 @@ const SoilDataDashboardWrapper = () => {
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'flex-start' }}>
{SOIL_CARD_IDS.map(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>
{SOIL_ROWS.map((rowCards, rowIndex) => {
const sizePerCard = 12 / rowCards.length
return (
<Grid
key={rowIndex}
size={12}
container
spacing={6}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
{rowCards.map(cardId => {
const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
</Grid>
</Box>
)
@@ -17,18 +17,17 @@ import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
// Service
import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
import type { CardId } from '@views/dashboards/farm/farmDashboardConfig'
import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig'
const WATER_CARD_IDS: CardId[] = [
'farmWeatherCard',
'farmAlertsTimeline',
'waterNeedPrediction',
'sensorValuesList'
/** هر ردیف: آرایهٔ کارت‌ها؛ در هر ردیف فضا مساوی بین گریدها تقسیم می‌شود (جمع = ۱۲) */
const WATER_ROWS: CardId[][] = [
['farmWeatherCard', 'farmAlertsTimeline', 'waterNeedPrediction'],
['sensorValuesList']
]
const cardRowSx = {
display: 'flex',
flexDirection: 'column',
minHeight: 380,
'& > *': { flex: 1, minHeight: 0 }
}
@@ -62,18 +61,28 @@ const WaterDataDashboardWrapper = () => {
return (
<Box position='relative'>
<Grid container spacing={6}>
<Grid size={12} container spacing={6} sx={{ display: 'flex', alignItems: 'flex-start' }}>
{WATER_CARD_IDS.map(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>
{WATER_ROWS.map((rowCards, rowIndex) => {
const sizePerCard = 12 / rowCards.length
return (
<Grid
key={rowIndex}
size={12}
container
spacing={6}
sx={{ display: 'flex', alignItems: 'stretch' }}
>
{rowCards.map(cardId => {
const Component = CARD_COMPONENTS[cardId]
if (!Component) return null
return (
<Grid key={cardId} size={sizePerCard} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
})}
</Grid>
)
})}
</Grid>
</Box>
)
@@ -19,6 +19,10 @@ type CropZoningMapProps = {
onZoneClick?: (zone: ZoneFeatureProperties) => void
optimizationKey?: number
className?: string
/** منطقهٔ اولیه از دیتای ماک؛ وقتی مقدار دارد نقشه فقط نمایشی است و کاربر نمیتواند منطقه را تغییر دهد */
initialAreaGeoJson?: MapDrawGeoJSON | null
/** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */
readOnly?: boolean
}
export default function CropZoningMap({
@@ -29,7 +33,9 @@ export default function CropZoningMap({
onAreaChange,
onZoneClick,
optimizationKey = 0,
className = ''
className = '',
initialAreaGeoJson = null,
readOnly = false
}: CropZoningMapProps) {
const mapRef = useRef<HTMLDivElement>(null)
const mapInstanceRef = useRef<L.Map | null>(null)
@@ -124,21 +130,22 @@ export default function CropZoningMap({
const drawnItems = L.featureGroup().addTo(map)
drawnItemsRef.current = drawnItems
const drawControl = new L.Control.Draw({
position: 'topright',
draw: {
polygon: { shapeOptions: { color: '#3388ff' } },
rectangle: { shapeOptions: { color: '#3388ff' } },
circle: false,
circlemarker: false,
marker: false,
polyline: false
},
edit: { featureGroup: drawnItems, remove: true }
})
map.addControl(drawControl)
drawControlRef.current = drawControl
if (!readOnly) {
const drawControl = new L.Control.Draw({
position: 'topright',
draw: {
polygon: { shapeOptions: { color: '#3388ff' } },
rectangle: { shapeOptions: { color: '#3388ff' } },
circle: false,
circlemarker: false,
marker: false,
polyline: false
},
edit: { featureGroup: drawnItems, remove: true }
})
map.addControl(drawControl)
drawControlRef.current = drawControl
}
const getGeoJsonFromDrawn = (): MapDrawGeoJSON => {
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 event = e as L.DrawEvents.Created
drawnItems.clearLayers()
@@ -175,17 +188,23 @@ export default function CropZoningMap({
}
}
map.on(L.Draw.Event.CREATED, onCreated)
map.on(L.Draw.Event.EDITED, onEdited)
map.on(L.Draw.Event.DELETED, onDeleted)
if (!readOnly) {
map.on(L.Draw.Event.CREATED, onCreated)
map.on(L.Draw.Event.EDITED, onEdited)
map.on(L.Draw.Event.DELETED, onDeleted)
}
mapInstanceRef.current = map
cleanupFn = () => {
map.off(L.Draw.Event.CREATED, onCreated)
map.off(L.Draw.Event.EDITED, onEdited)
map.off(L.Draw.Event.DELETED, onDeleted)
map.removeControl(drawControl)
if (!readOnly) {
map.off(L.Draw.Event.CREATED, onCreated)
map.off(L.Draw.Event.EDITED, onEdited)
map.off(L.Draw.Event.DELETED, onDeleted)
if (drawControlRef.current) {
map.removeControl(drawControlRef.current)
}
}
if (zonesLayerRef.current) map.removeLayer(zonesLayerRef.current)
map.remove()
mapInstanceRef.current = null
@@ -96,64 +96,77 @@ export default function CropZoningWeatherSection() {
const humidity = (weatherData.humidity as number) ?? 45
const windSpeed = (weatherData.windSpeed as number) ?? 12
const cardRowSx = {
display: 'flex',
flexDirection: 'column' as const,
minHeight: 200,
'& > *': { flex: 1, minHeight: 0 }
}
return (
<Box className='pis-0 pie-0'>
<Typography variant='h6' className='mbe-4 font-semibold'>
{t('title')}
</Typography>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
<FarmWeatherCard data={weatherData} />
{/* Row 1: Weather + 3 KPI cards — equal width (3 each on md+) */}
<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 size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<Card>
<CardContent className='flex flex-col items-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: 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>
{/* Row 2: Forecast chart — full width */}
<Grid size={12} sx={cardRowSx}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 280 }}>
<CardHeader
title={t('forecastChart')}
subheader={t('forecastSubheader')}
/>
<CardContent>
<CardContent sx={{ flex: 1, minHeight: 0 }}>
<AppReactApexCharts
type='line'
height={260}
@@ -11,6 +11,7 @@ import ZoneLegend from './ZoneLegend'
import LayerControl from './LayerControl'
import ZoneDetailPanel from './ZoneDetailPanel'
import CropZoningWeatherSection from './CropZoningWeatherSection'
import { MOCK_AREA_GEOJSON } from './cropZoningMockData'
import type { LayerType } from './cropZoningTypes'
import type { ZoneFeatureProperties } from './cropZoningTypes'
import type { MapDrawGeoJSON } from './CropZoningMap'
@@ -26,7 +27,7 @@ const MapComponent = dynamic(() => Promise.resolve(CropZoningMap), {
export default function CropZoningWrapper() {
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 [selectedZone, setSelectedZone] = useState<ZoneFeatureProperties | null>(null)
const [panelOpen, setPanelOpen] = useState(false)
@@ -58,6 +59,8 @@ export default function CropZoningWrapper() {
onZoneClick={handleZoneClick}
optimizationKey={optimizationKey}
className='min-bs-[400px]'
initialAreaGeoJson={MOCK_AREA_GEOJSON}
readOnly
/>
</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 = {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 0 },
plugins: {
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 ───────────────────────────────────────────────────────────
@@ -715,12 +723,12 @@ export default function PlantSimulator() {
]
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'>
🌱 {t('title')}
</Typography>
<Grid container spacing={6} className='max-w-6xl mx-auto'>
<Grid container spacing={6} className='min-is-0 is-full'>
{/* ── Left: Plant visualization ── */}
<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'>