Refactor CropZoningMap and related components for improved API integration and UI enhancements
- Updated CropZoningMap to utilize new ZoneInitialData type for zone click handling and added zonesData prop for API-driven zone rendering. - Removed deprecated crop zoning mock data file and integrated grid creation logic for initial zone fetching. - Enhanced CropZoningWrapper to manage area and zone data loading states, improving user experience with asynchronous data fetching. - Updated ZoneDetailPanel to handle loading states and display product labels dynamically based on fetched data. - Refactored ZoneLegend to conditionally render items based on available product data, enhancing visual feedback during loading.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Box from '@mui/material/Box'
|
||||
@@ -11,9 +11,9 @@ import ZoneLegend from './ZoneLegend'
|
||||
import LayerControl from './LayerControl'
|
||||
import ZoneDetailPanel from './ZoneDetailPanel'
|
||||
import CropZoningWeatherSection from './CropZoningWeatherSection'
|
||||
import { MOCK_AREA_GEOJSON } from './cropZoningMockData'
|
||||
import { createGridFromPolygon } from './cropZoningUtils'
|
||||
import { cropZoningService, type Product, type ZoneInitialData, type ZoneDetailData } from '@/libs/api/services/cropZoningService'
|
||||
import type { LayerType } from './cropZoningTypes'
|
||||
import type { ZoneFeatureProperties } from './cropZoningTypes'
|
||||
import type { MapDrawGeoJSON } from './CropZoningMap'
|
||||
|
||||
const MapComponent = dynamic(() => Promise.resolve(CropZoningMap), {
|
||||
@@ -25,21 +25,78 @@ const MapComponent = dynamic(() => Promise.resolve(CropZoningMap), {
|
||||
)
|
||||
})
|
||||
|
||||
function isPolygon(geojson: MapDrawGeoJSON): geojson is MapDrawGeoJSON & { geometry: { type: 'Polygon'; coordinates: unknown[] } } {
|
||||
return !!geojson?.geometry && (geojson.geometry as { type: string }).type === 'Polygon'
|
||||
}
|
||||
|
||||
export default function CropZoningWrapper() {
|
||||
const t = useTranslations('cropZoning')
|
||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(MOCK_AREA_GEOJSON)
|
||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
|
||||
const [areaLoading, setAreaLoading] = useState(true)
|
||||
const [zonesData, setZonesData] = useState<ZoneInitialData[] | null>(null)
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [productsLoading, setProductsLoading] = useState(true)
|
||||
const [zonesLoading, setZonesLoading] = useState(false)
|
||||
const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
|
||||
const [selectedZone, setSelectedZone] = useState<ZoneFeatureProperties | null>(null)
|
||||
const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null)
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
const [zoneDetailLoading, setZoneDetailLoading] = useState(false)
|
||||
const [optimizationKey, setOptimizationKey] = useState(0)
|
||||
|
||||
const productLabels = Object.fromEntries(products.map(p => [p.id, p.label]))
|
||||
|
||||
useEffect(() => {
|
||||
cropZoningService
|
||||
.getProducts()
|
||||
.then(res => setProducts(res.products))
|
||||
.catch(() => setProducts([]))
|
||||
.finally(() => setProductsLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setAreaLoading(true)
|
||||
cropZoningService
|
||||
.getArea()
|
||||
.then(res => setAreaGeoJson(res.area as unknown as MapDrawGeoJSON))
|
||||
.catch(() => setAreaGeoJson(null))
|
||||
.finally(() => setAreaLoading(false))
|
||||
}, [])
|
||||
|
||||
const fetchZones = useCallback((geojson: MapDrawGeoJSON) => {
|
||||
if (!isPolygon(geojson)) {
|
||||
setZonesData(null)
|
||||
return
|
||||
}
|
||||
setZonesLoading(true)
|
||||
const grid = createGridFromPolygon(geojson as unknown as import('geojson').Feature<import('geojson').Polygon>)
|
||||
cropZoningService
|
||||
.getZonesInitial({ zones: grid })
|
||||
.then(res => setZonesData(res.zones))
|
||||
.catch(() => setZonesData(null))
|
||||
.finally(() => setZonesLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPolygon(areaGeoJson)) {
|
||||
fetchZones(areaGeoJson)
|
||||
} else {
|
||||
setZonesData(null)
|
||||
}
|
||||
}, [areaGeoJson, optimizationKey, fetchZones])
|
||||
|
||||
const handleAreaChange = useCallback((geojson: MapDrawGeoJSON) => {
|
||||
setAreaGeoJson(geojson)
|
||||
}, [])
|
||||
|
||||
const handleZoneClick = useCallback((zone: ZoneFeatureProperties) => {
|
||||
setSelectedZone(zone)
|
||||
const handleZoneClick = useCallback((zone: ZoneInitialData) => {
|
||||
setZoneDetailLoading(true)
|
||||
setPanelOpen(true)
|
||||
setSelectedZone(null)
|
||||
cropZoningService
|
||||
.getZoneDetails(zone.zoneId)
|
||||
.then(details => setSelectedZone(details))
|
||||
.catch(() => setSelectedZone(null))
|
||||
.finally(() => setZoneDetailLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleOptimize = useCallback(() => {
|
||||
@@ -50,46 +107,64 @@ export default function CropZoningWrapper() {
|
||||
<Box className='flex flex-col gap-6 is-full'>
|
||||
<Box className='relative min-bs-[400px] rounded-xl overflow-hidden' sx={{ height: 'min(60vh, 500px)' }}>
|
||||
<Box className='absolute inset-0 z-0'>
|
||||
<MapComponent
|
||||
center={[35.6892, 51.389]}
|
||||
zoom={13}
|
||||
height='100%'
|
||||
activeLayer={activeLayer}
|
||||
onAreaChange={handleAreaChange}
|
||||
onZoneClick={handleZoneClick}
|
||||
optimizationKey={optimizationKey}
|
||||
className='min-bs-[400px]'
|
||||
initialAreaGeoJson={MOCK_AREA_GEOJSON}
|
||||
readOnly
|
||||
/>
|
||||
</Box>
|
||||
{areaGeoJson ? (
|
||||
<MapComponent
|
||||
key='crop-zoning-map'
|
||||
center={[35.6892, 51.389]}
|
||||
zoom={13}
|
||||
height='100%'
|
||||
activeLayer={activeLayer}
|
||||
onAreaChange={handleAreaChange}
|
||||
onZoneClick={handleZoneClick}
|
||||
optimizationKey={optimizationKey}
|
||||
className='min-bs-[400px]'
|
||||
initialAreaGeoJson={areaGeoJson}
|
||||
zonesData={zonesData}
|
||||
productLabels={productLabels}
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<Box className='flex items-center justify-center bs-full min-bs-[400px] rounded-xl bg-action-hover' />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
||||
{(areaLoading || zonesLoading) && (
|
||||
<Box
|
||||
className='absolute inset-0 z-[500] flex items-center justify-center bg-white/60 dark:bg-gray-900/60'
|
||||
sx={{ borderRadius: 12 }}
|
||||
>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{areaGeoJson && (
|
||||
<>
|
||||
<ZoneLegend />
|
||||
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
||||
|
||||
<ZoneLegend products={products} loading={productsLoading} />
|
||||
|
||||
{areaGeoJson && (
|
||||
<Box className='absolute top-16 end-4 z-[1000]'>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='medium'
|
||||
startIcon={<i className='tabler-refresh text-xl' />}
|
||||
onClick={handleOptimize}
|
||||
className='rounded-xl shadow-lg'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='medium'
|
||||
startIcon={<i className='tabler-refresh text-xl' />}
|
||||
onClick={handleOptimize}
|
||||
disabled={zonesLoading}
|
||||
className='rounded-xl shadow-lg'
|
||||
>
|
||||
{t('optimizeAgain')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ZoneDetailPanel
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
zone={selectedZone}
|
||||
products={products}
|
||||
loading={zoneDetailLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<CropZoningWeatherSection />
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user