Remove API documentation file and update navigation constants with new entries for farm dashboard, water data, soil data, and data section. Enhance sensor hub functionality by adding new sensor payload structure and integrating plant type and name selection in the sensor form. Refactor calendar components to streamline code and improve maintainability.

This commit is contained in:
2026-02-20 20:24:24 +03:30
parent 4424fc8e87
commit 890599b0e7
17 changed files with 822 additions and 1925 deletions
-1724
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1 +1,3 @@
declare module 'tailwindcss-logical' declare module 'tailwindcss-logical'
declare module 'leaflet/dist/leaflet.css'
declare module 'leaflet-draw/dist/leaflet.draw.css'
+6
View File
@@ -33,6 +33,10 @@
}, },
"navigation": { "navigation": {
"dashboards": "داشبوردها", "dashboards": "داشبوردها",
"farm": "داشبورد مزرعه",
"waterData": "دیتاهای آب",
"soilData": "اطلاعات خاک",
"dataSection": "بخش داده‌ها",
"crm": "مدیریت ارتباط با مشتری", "crm": "مدیریت ارتباط با مشتری",
"analytics": "تحلیل‌ها", "analytics": "تحلیل‌ها",
"eCommerce": "فروشگاه", "eCommerce": "فروشگاه",
@@ -267,6 +271,8 @@
"sensorUuid": "شناسه سنسور (UUID)", "sensorUuid": "شناسه سنسور (UUID)",
"placeholderName": "نام سنسور را وارد کنید", "placeholderName": "نام سنسور را وارد کنید",
"placeholderUuid": "شناسه سنسور را وارد کنید", "placeholderUuid": "شناسه سنسور را وارد کنید",
"plantType": "نوع گیاه",
"plantName": "اسم گیاه",
"saveSensor": "ذخیره سنسور", "saveSensor": "ذخیره سنسور",
"saving": "در حال ذخیره...", "saving": "در حال ذخیره...",
"errorSave": "خطا در ذخیره سنسور", "errorSave": "خطا در ذخیره سنسور",
+91
View File
@@ -42,6 +42,8 @@
"@tiptap/pm": "^2.10.4", "@tiptap/pm": "^2.10.4",
"@tiptap/react": "^2.10.4", "@tiptap/react": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4", "@tiptap/starter-kit": "^2.10.4",
"@types/leaflet": "^1.9.21",
"@types/leaflet-draw": "^1.0.13",
"apexcharts": "3.49.0", "apexcharts": "3.49.0",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"classnames": "2.5.1", "classnames": "2.5.1",
@@ -51,6 +53,8 @@
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"keen-slider": "6.8.6", "keen-slider": "6.8.6",
"leaflet": "^1.9.4",
"leaflet-draw": "^1.0.4",
"mapbox-gl": "3.9.0", "mapbox-gl": "3.9.0",
"negotiator": "1.0.0", "negotiator": "1.0.0",
"next": "15.1.2", "next": "15.1.2",
@@ -63,7 +67,9 @@
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-dropzone": "14.3.5", "react-dropzone": "14.3.5",
"react-hook-form": "7.54.1", "react-hook-form": "7.54.1",
"react-leaflet": "^4.2.1",
"react-map-gl": "7.1.8", "react-map-gl": "7.1.8",
"react-multi-date-picker": "^4.5.2",
"react-perfect-scrollbar": "1.5.8", "react-perfect-scrollbar": "1.5.8",
"react-player": "2.16.0", "react-player": "2.16.0",
"react-redux": "9.2.0", "react-redux": "9.2.0",
@@ -2552,6 +2558,17 @@
} }
} }
}, },
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://mirror-npm.runflare.com/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://mirror-npm.runflare.com/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", "resolved": "https://mirror-npm.runflare.com/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
@@ -3184,6 +3201,24 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://mirror-npm.runflare.com/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/leaflet-draw": {
"version": "1.0.13",
"resolved": "https://mirror-npm.runflare.com/@types/leaflet-draw/-/leaflet-draw-1.0.13.tgz",
"integrity": "sha512-YU82kilOaU+wPNbqKCCDfHH3hqepN6XilrBwG/mSeZ/z4ewumaRCOah44s3FMxSu/Aa0SVa3PPJvhIZDUA09mw==",
"license": "MIT",
"dependencies": {
"@types/leaflet": "^1.9"
}
},
"node_modules/@types/linkify-it": { "node_modules/@types/linkify-it": {
"version": "5.0.0", "version": "5.0.0",
"license": "MIT" "license": "MIT"
@@ -7154,6 +7189,18 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://mirror-npm.runflare.com/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/leaflet-draw": {
"version": "1.0.4",
"resolved": "https://mirror-npm.runflare.com/leaflet-draw/-/leaflet-draw-1.0.4.tgz",
"integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==",
"license": "MIT"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"dev": true, "dev": true,
@@ -8650,6 +8697,16 @@
"react": ">= 16.8 || 18.0.0" "react": ">= 16.8 || 18.0.0"
} }
}, },
"node_modules/react-element-popper": {
"version": "2.1.7",
"resolved": "https://mirror-npm.runflare.com/react-element-popper/-/react-element-popper-2.1.7.tgz",
"integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-fast-compare": { "node_modules/react-fast-compare": {
"version": "3.2.2", "version": "3.2.2",
"license": "MIT" "license": "MIT"
@@ -8672,6 +8729,20 @@
"version": "19.0.0", "version": "19.0.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-leaflet": {
"version": "4.2.1",
"resolved": "https://mirror-npm.runflare.com/react-leaflet/-/react-leaflet-4.2.1.tgz",
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^2.1.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-map-gl": { "node_modules/react-map-gl": {
"version": "7.1.8", "version": "7.1.8",
"license": "MIT", "license": "MIT",
@@ -8694,6 +8765,26 @@
} }
} }
}, },
"node_modules/react-multi-date-picker": {
"version": "4.5.2",
"resolved": "https://mirror-npm.runflare.com/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz",
"integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==",
"license": "MIT",
"dependencies": {
"react-date-object": "^2.1.8",
"react-element-popper": "^2.1.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-multi-date-picker/node_modules/react-date-object": {
"version": "2.1.9",
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz",
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
"license": "MIT"
},
"node_modules/react-onclickoutside": { "node_modules/react-onclickoutside": {
"version": "6.13.1", "version": "6.13.1",
"license": "MIT", "license": "MIT",
+6
View File
@@ -47,6 +47,8 @@
"@tiptap/pm": "^2.10.4", "@tiptap/pm": "^2.10.4",
"@tiptap/react": "^2.10.4", "@tiptap/react": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4", "@tiptap/starter-kit": "^2.10.4",
"@types/leaflet": "^1.9.21",
"@types/leaflet-draw": "^1.0.13",
"apexcharts": "3.49.0", "apexcharts": "3.49.0",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"classnames": "2.5.1", "classnames": "2.5.1",
@@ -56,6 +58,8 @@
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"keen-slider": "6.8.6", "keen-slider": "6.8.6",
"leaflet": "^1.9.4",
"leaflet-draw": "^1.0.4",
"mapbox-gl": "3.9.0", "mapbox-gl": "3.9.0",
"negotiator": "1.0.0", "negotiator": "1.0.0",
"next": "15.1.2", "next": "15.1.2",
@@ -68,7 +72,9 @@
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-dropzone": "14.3.5", "react-dropzone": "14.3.5",
"react-hook-form": "7.54.1", "react-hook-form": "7.54.1",
"react-leaflet": "^4.2.1",
"react-map-gl": "7.1.8", "react-map-gl": "7.1.8",
"react-multi-date-picker": "^4.5.2",
"react-perfect-scrollbar": "1.5.8", "react-perfect-scrollbar": "1.5.8",
"react-player": "2.16.0", "react-player": "2.16.0",
"react-redux": "9.2.0", "react-redux": "9.2.0",
@@ -0,0 +1,8 @@
// Components Imports
import SoilDataDashboardWrapper from '@views/dashboards/farm/SoilDataDashboardWrapper'
const SoilDataPage = async () => {
return <SoilDataDashboardWrapper />
}
export default SoilDataPage
@@ -0,0 +1,8 @@
// Components Imports
import WaterDataDashboardWrapper from '@views/dashboards/farm/WaterDataDashboardWrapper'
const WaterDataPage = async () => {
return <WaterDataDashboardWrapper />
}
export default WaterDataPage
+172
View File
@@ -0,0 +1,172 @@
'use client'
import { useEffect, useRef } from 'react'
import type L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import 'leaflet-draw/dist/leaflet.draw.css'
export type MapDrawGeoJSON = Record<string, unknown> | null
type MapDrawProps = {
center?: [number, number]
zoom?: number
height?: string | number
onAreaChange?: (geojson: MapDrawGeoJSON, area?: number) => void
initialGeoJson?: MapDrawGeoJSON
singleShape?: boolean
}
export default function MapDraw({
center = [35.6892, 51.389],
zoom = 13,
height = '400px',
onAreaChange,
initialGeoJson,
singleShape = true
}: MapDrawProps) {
const mapRef = useRef<HTMLDivElement>(null)
const mapInstanceRef = useRef<L.Map | null>(null)
const drawnItemsRef = useRef<L.FeatureGroup | null>(null)
const drawControlRef = useRef<L.Control.Draw | null>(null)
useEffect(() => {
if (typeof window === 'undefined' || !mapRef.current) return
let isMounted = true
let cleanupFn: (() => void) | null = null
const initMap = async () => {
const L = (await import('leaflet')).default
await import('leaflet-draw')
if (!isMounted || !mapRef.current || mapInstanceRef.current) return
const map = L.map(mapRef.current).setView(center, zoom)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map)
const drawnItems = L.featureGroup().addTo(map)
drawnItemsRef.current = drawnItems
if (initialGeoJson && drawnItems) {
try {
const layer = L.geoJSON(initialGeoJson as never).eachLayer(l => {
drawnItems.addLayer(l)
})
if (layer.getBounds && layer.getBounds().isValid()) {
map.fitBounds(layer.getBounds())
}
} catch {
// ignore invalid geojson
}
}
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()
if (geojson.type === 'FeatureCollection' && geojson.features.length > 0) {
return geojson.features[0] as unknown as MapDrawGeoJSON ?? null
}
return null
}
const getArea = (layer: L.Layer): number | undefined => {
const poly = layer as L.Polygon
if (poly.getLatLngs && typeof L.GeometryUtil?.geodesicArea === 'function') {
const latlngs = poly.getLatLngs() as L.LatLng[]
return L.GeometryUtil.geodesicArea(Array.isArray(latlngs[0]) ? latlngs[0] : latlngs)
}
return undefined
}
const emitChange = () => {
const geojson = getGeoJsonFromDrawn()
let area: number | undefined
drawnItems.eachLayer(layer => {
const a = getArea(layer)
if (a !== undefined) area = (area ?? 0) + a
})
onAreaChange?.(geojson, area)
}
const onCreated = (e: L.LeafletEvent) => {
const event = e as L.DrawEvents.Created
const layer = event.layer
if (singleShape) {
drawnItems.clearLayers()
}
drawnItems.addLayer(layer)
emitChange()
}
const onEdited = () => emitChange()
const onDeleted = () => emitChange()
map.on(L.Draw.Event.CREATED, onCreated)
map.on(L.Draw.Event.EDITED, onEdited)
map.on(L.Draw.Event.DELETED, onDeleted)
mapInstanceRef.current = map
if (initialGeoJson && drawnItems.getLayers().length > 0) {
emitChange()
}
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)
map.remove()
mapInstanceRef.current = null
drawnItemsRef.current = null
drawControlRef.current = null
}
}
initMap()
return () => {
isMounted = false
if (cleanupFn) {
cleanupFn()
} else if (mapInstanceRef.current) {
mapInstanceRef.current.remove()
mapInstanceRef.current = null
drawnItemsRef.current = null
drawControlRef.current = null
}
}
}, [])
return (
<div
ref={mapRef}
style={{ height: typeof height === 'number' ? `${height}px` : height, width: '100%', borderRadius: 8 }}
className='overflow-hidden'
/>
)
}
+2
View File
@@ -0,0 +1,2 @@
export { default as MapDraw } from './MapDraw'
export type { MapDrawGeoJSON } from './MapDraw'
@@ -94,10 +94,16 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
> >
{t('dashboards')} {t('dashboards')}
</MenuItem> </MenuItem>
<MenuSection label={t('dataSection')}>
<MenuItem href="/dashboard/water-data" icon={<i className="tabler-droplet" />}>
{t('waterData')}
</MenuItem>
<MenuItem href="/dashboard/soil-data" icon={<i className="tabler-seedling" />}>
{t('soilData')}
</MenuItem>
</MenuSection>
{/* </MenuSection> */}
</Menu> </Menu>
{/* <Menu {/* <Menu
popoutMenuOffset={{ mainAxis: 23 }} popoutMenuOffset={{ mainAxis: 23 }}
+4
View File
@@ -1,6 +1,10 @@
// Navigation labels - hardcoded for RTL project // Navigation labels - hardcoded for RTL project
export const navigationLabels = { export const navigationLabels = {
dashboards: 'داشبوردها', dashboards: 'داشبوردها',
farm: 'داشبورد مزرعه',
waterData: 'دیتاهای آب',
soilData: 'اطلاعات خاک',
dataSection: 'بخش داده‌ها',
crm: 'مدیریت ارتباط با مشتری', crm: 'مدیریت ارتباط با مشتری',
analytics: 'تحلیل‌ها', analytics: 'تحلیل‌ها',
eCommerce: 'فروشگاه', eCommerce: 'فروشگاه',
+16
View File
@@ -17,7 +17,23 @@ export interface ListSensorsResponse {
data: Sensor | Sensor[] data: Sensor | Sensor[]
} }
export interface AddSensorPayload {
name: string
uuid_sensor: string
area_geojson?: Record<string, unknown>
area_m2?: number
}
export const sensorHubService = { export const sensorHubService = {
/**
* Add a new sensor
*/
async addSensor(payload: AddSensorPayload): Promise<Sensor> {
const response = await apiClient.post<{ data?: Sensor }>('/api/sensor-hub/', payload)
return (response?.data as Sensor) ?? (payload as unknown as Sensor)
},
/** /**
* Get list of sensors * Get list of sensors
*/ */
+47 -79
View File
@@ -1,90 +1,58 @@
'use client' 'use client'
// React Imports
import { useState, useEffect } from 'react'
// MUI Imports
import { useMediaQuery } from '@mui/material'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import { useSelector } from 'react-redux'
// Type Imports
import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
// Redux Imports
import { useAppDispatch } from '@/redux-store'
// Slice Imports
import { fetchEvents } from '@/redux-store/slices/calendar'
// Component Imports
import Calendar from './Calendar'
import SidebarLeft from './SidebarLeft'
import AddEventSidebar from './AddEventSidebar'
// CalendarColors Object
const calendarsColor: CalendarColors = {
Personal: 'error',
Business: 'primary',
Family: 'warning',
Holiday: 'success',
ETC: 'info'
}
const AppCalendar = () => { const AppCalendar = () => {
// States return <></>
const [calendarApi, setCalendarApi] = useState<null | any>(null) // // States
const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false) // const [calendarApi, setCalendarApi] = useState<null | any>(null)
const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false) // const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
// const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
// Hooks // // Hooks
const dispatch = useAppDispatch() // const dispatch = useAppDispatch()
const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer) // const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer)
const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) // const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
// Fetch events on mount // // Fetch events on mount
useEffect(() => { // useEffect(() => {
dispatch(fetchEvents()) // dispatch(fetchEvents())
}, [dispatch]) // }, [dispatch])
const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen) // const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen) // const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
return ( // return (
<> // <>
<SidebarLeft // <SidebarLeft
mdAbove={mdAbove} // mdAbove={mdAbove}
dispatch={dispatch} // dispatch={dispatch}
calendarApi={calendarApi} // calendarApi={calendarApi}
calendarStore={calendarStore} // calendarStore={calendarStore}
calendarsColor={calendarsColor} // calendarsColor={calendarsColor}
leftSidebarOpen={leftSidebarOpen} // leftSidebarOpen={leftSidebarOpen}
handleLeftSidebarToggle={handleLeftSidebarToggle} // handleLeftSidebarToggle={handleLeftSidebarToggle}
handleAddEventSidebarToggle={handleAddEventSidebarToggle} // handleAddEventSidebarToggle={handleAddEventSidebarToggle}
/> // />
<div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'> // <div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
<Calendar // <Calendar
dispatch={dispatch} // dispatch={dispatch}
calendarApi={calendarApi} // calendarApi={calendarApi}
calendarStore={calendarStore} // calendarStore={calendarStore}
setCalendarApi={setCalendarApi} // setCalendarApi={setCalendarApi}
calendarsColor={calendarsColor} // calendarsColor={calendarsColor}
handleLeftSidebarToggle={handleLeftSidebarToggle} // handleLeftSidebarToggle={handleLeftSidebarToggle}
handleAddEventSidebarToggle={handleAddEventSidebarToggle} // handleAddEventSidebarToggle={handleAddEventSidebarToggle}
/> // />
</div> // </div>
<AddEventSidebar // <AddEventSidebar
dispatch={dispatch} // dispatch={dispatch}
calendarApi={calendarApi} // calendarApi={calendarApi}
calendarStore={calendarStore} // calendarStore={calendarStore}
addEventSidebarOpen={addEventSidebarOpen} // addEventSidebarOpen={addEventSidebarOpen}
handleAddEventSidebarToggle={handleAddEventSidebarToggle} // handleAddEventSidebarToggle={handleAddEventSidebarToggle}
/> // />
</> // </>
) // )
} }
export default AppCalendar export default AppCalendar
+195 -112
View File
@@ -1,23 +1,6 @@
// MUI Imports
import Button from '@mui/material/Button'
import Drawer from '@mui/material/Drawer'
import Divider from '@mui/material/Divider'
import Checkbox from '@mui/material/Checkbox'
import Typography from '@mui/material/Typography'
import FormControlLabel from '@mui/material/FormControlLabel'
// Third-party imports
import classnames from 'classnames'
// Types Imports // Types Imports
import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes' import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes'
import type { ThemeColor } from '@core/types'
// Styled Component Imports
import AppJalaliDatepicker from '@/libs/styles/AppJalaliDatepicker'
// Slice Imports
import { filterAllCalendarLabels, filterCalendarLabel, selectedEvent } from '@/redux-store/slices/calendar'
const SidebarLeft = (props: SidebarLeftProps) => { const SidebarLeft = (props: SidebarLeftProps) => {
// Props // Props
@@ -32,105 +15,205 @@ const SidebarLeft = (props: SidebarLeftProps) => {
handleAddEventSidebarToggle handleAddEventSidebarToggle
} = props } = props
// Vars // // Vars
const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] // const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
const renderFilters = colorsArr.length // const renderFilters = colorsArr.length
? colorsArr.map(([key, value]: string[]) => { // ? colorsArr.map(([key, value]: string[]) => {
return ( // return (
<FormControlLabel // <FormControlLabel
className='mbe-1' // className='mbe-1'
key={key} // key={key}
label={key} // label={key}
control={ // control={
<Checkbox // <Checkbox
color={value as ThemeColor} // color={value as ThemeColor}
checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1} // checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} // onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
/> // />
} // }
/> // />
) // )
}) // })
: null // : null
const handleSidebarToggleSidebar = () => { // const handleSidebarToggleSidebar = () => {
dispatch(selectedEvent(null)) // dispatch(selectedEvent(null))
handleAddEventSidebarToggle() // handleAddEventSidebarToggle()
} // }
if (renderFilters) { // if (renderFilters) {
return ( // return (
<Drawer // <Drawer
open={leftSidebarOpen} // open={leftSidebarOpen}
onClose={handleLeftSidebarToggle} // onClose={handleLeftSidebarToggle}
variant={mdAbove ? 'permanent' : 'temporary'} // variant={mdAbove ? 'permanent' : 'temporary'}
ModalProps={{ // ModalProps={{
disablePortal: true, // disablePortal: true,
disableAutoFocus: true, // disableAutoFocus: true,
disableScrollLock: true, // disableScrollLock: true,
keepMounted: true // Better open performance on mobile. // keepMounted: true // Better open performance on mobile.
}} // }}
className={classnames('block', { static: mdAbove, absolute: !mdAbove })} // className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
PaperProps={{ // PaperProps={{
className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', { // className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
static: mdAbove, // static: mdAbove,
absolute: !mdAbove // absolute: !mdAbove
}) // })
}} // }}
sx={{ // sx={{
zIndex: 3, // zIndex: 3,
'& .MuiDrawer-paper': { // '& .MuiDrawer-paper': {
zIndex: mdAbove ? 2 : 'drawer' // zIndex: mdAbove ? 2 : 'drawer'
}, // },
'& .MuiBackdrop-root': { // '& .MuiBackdrop-root': {
borderRadius: 1, // borderRadius: 1,
position: 'absolute' // position: 'absolute'
} // }
}} // }}
> // >
<div className='is-full p-6'> // <div className='is-full p-6'>
<Button // <Button
fullWidth // fullWidth
variant='contained' // variant='contained'
onClick={handleSidebarToggleSidebar} // onClick={handleSidebarToggleSidebar}
startIcon={<i className='tabler-plus' />} // startIcon={<i className='tabler-plus' />}
> // >
Add Event // Add Event
</Button> // </Button>
</div> // </div>
<Divider className='is-full' /> // <Divider className='is-full' />
<AppJalaliDatepicker // <AppJalaliDatepicker
onChange={date => calendarApi?.gotoDate(date)} // onChange={date => calendarApi?.gotoDate(date)}
boxProps={{ // boxProps={{
className: 'flex justify-center is-full', // className: 'flex justify-center is-full',
sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } // sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
}} // }}
/> // />
<Divider className='is-full' /> // <Divider className='is-full' />
<div className='flex flex-col p-6 is-full'> // <div className='flex flex-col p-6 is-full'>
<Typography variant='h5' className='mbe-4'> // <Typography variant='h5' className='mbe-4'>
Event Filters // Event Filters
</Typography> // </Typography>
<FormControlLabel // <FormControlLabel
className='mbe-1' // className='mbe-1'
label='View All' // label='View All'
control={ // control={
<Checkbox // <Checkbox
color='secondary' // color='secondary'
checked={calendarStore.selectedCalendars.length === colorsArr.length} // checked={calendarStore.selectedCalendars.length === colorsArr.length}
onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))} // onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
/> // />
} // }
/> // />
{renderFilters} // {renderFilters}
</div> // </div>
</Drawer> // </Drawer>
) // )
} else { // } else {
return null // return null
} // }
// // Vars
// const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
// const renderFilters = colorsArr.length
// ? colorsArr.map(([key, value]: string[]) => {
// return (
// <FormControlLabel
// className='mbe-1'
// key={key}
// label={key}
// control={
// <Checkbox
// color={value as ThemeColor}
// checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
// onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
// />
// }
// />
// )
// })
// : null
// const handleSidebarToggleSidebar = () => {
// dispatch(selectedEvent(null))
// handleAddEventSidebarToggle()
// }
// if (renderFilters) {
// return (
// <Drawer
// open={leftSidebarOpen}
// onClose={handleLeftSidebarToggle}
// variant={mdAbove ? 'permanent' : 'temporary'}
// ModalProps={{
// disablePortal: true,
// disableAutoFocus: true,
// disableScrollLock: true,
// keepMounted: true // Better open performance on mobile.
// }}
// className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
// PaperProps={{
// className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
// static: mdAbove,
// absolute: !mdAbove
// })
// }}
// sx={{
// zIndex: 3,
// '& .MuiDrawer-paper': {
// zIndex: mdAbove ? 2 : 'drawer'
// },
// '& .MuiBackdrop-root': {
// borderRadius: 1,
// position: 'absolute'
// }
// }}
// >
// <div className='is-full p-6'>
// <Button
// fullWidth
// variant='contained'
// onClick={handleSidebarToggleSidebar}
// startIcon={<i className='tabler-plus' />}
// >
// Add Event
// </Button>
// </div>
// <Divider className='is-full' />
// <AppJalaliDatepicker
// onChange={date => calendarApi?.gotoDate(date)}
// boxProps={{
// className: 'flex justify-center is-full',
// sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
// }}
// />
// <Divider className='is-full' />
// <div className='flex flex-col p-6 is-full'>
// <Typography variant='h5' className='mbe-4'>
// Event Filters
// </Typography>
// <FormControlLabel
// className='mbe-1'
// label='View All'
// control={
// <Checkbox
// color='secondary'
// checked={calendarStore.selectedCalendars.length === colorsArr.length}
// onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
// />
// }
// />
// {renderFilters}
// </div>
// </Drawer>
// )
// } else {
// return null
// }
return <></>
} }
export default SidebarLeft export default SidebarLeft
@@ -0,0 +1,85 @@
'use client'
// React Imports
import { useEffect, useState } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
// Component Imports
import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap'
import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart'
import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart'
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 cardRowSx = {
display: 'flex',
flexDirection: 'column',
'& > *': { flex: 1, minHeight: 0 }
}
const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Record<string, unknown> }>>> = {
soilMoistureHeatmap: SoilMoistureHeatmap,
sensorValuesList: SensorValuesList,
sensorComparisonChart: SensorComparisonChart,
sensorRadarChart: SensorRadarChart,
anomalyDetectionCard: AnomalyDetectionCard
}
const SoilDataDashboardWrapper = () => {
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
farmDashboardService
.getAllCards()
.then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({}))
.finally(() => setLoading(false))
}, [])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
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>
</Grid>
</Box>
)
}
export default SoilDataDashboardWrapper
@@ -0,0 +1,82 @@
'use client'
// React Imports
import { useEffect, useState } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
// Component Imports
import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard'
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction'
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 cardRowSx = {
display: 'flex',
flexDirection: 'column',
'& > *': { flex: 1, minHeight: 0 }
}
const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Record<string, unknown> }>>> = {
farmWeatherCard: FarmWeatherCard,
farmAlertsTimeline: FarmAlertsTimeline,
waterNeedPrediction: WaterNeedPrediction,
sensorValuesList: SensorValuesList
}
const WaterDataDashboardWrapper = () => {
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
farmDashboardService
.getAllCards()
.then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({}))
.finally(() => setLoading(false))
}, [])
if (loading) {
return (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<CircularProgress />
</Box>
)
}
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>
</Grid>
</Box>
)
}
export default WaterDataDashboardWrapper
+84 -2
View File
@@ -1,7 +1,8 @@
'use client' 'use client'
// React Imports // React Imports
import { useState } from 'react' import { useState, useCallback } from 'react'
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
// MUI Imports // MUI Imports
@@ -9,30 +10,72 @@ import Grid from '@mui/material/Grid2'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
import Alert from '@mui/material/Alert' import Alert from '@mui/material/Alert'
import Typography from '@mui/material/Typography'
// API Imports // API Imports
import { sensorHubService } from '@/libs/api' import { sensorHubService } from '@/libs/api'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import CustomAutocomplete from '@core/components/mui/Autocomplete'
import type { MapDrawGeoJSON } from '@/components/MapDraw'
const MapDraw = dynamic(() => import('@/components/MapDraw').then(mod => mod.MapDraw), {
ssr: false,
loading: () => (
<div className='flex items-center justify-center h-[400px] rounded-lg bg-action-hover'>
<CircularProgress size={32} />
</div>
)
})
type FormSensorHubProps = { type FormSensorHubProps = {
onBack: () => void onBack: () => void
} }
const PLANT_TYPES = ['گندم', 'جو', 'ذرت', 'برنج', 'پنبه', 'چغندر قند', 'سیب‌زمینی', 'گوجه‌فرنگی', 'پیاز', 'سبزیجات']
const PLANT_NAMES_BY_TYPE: Record<string, string[]> = {
گندم: ['رقم آذر', 'رقم شریف', 'رقم مروارید', 'رقم بهار', 'رقم الوند'],
جو: ['رقم سرداری', 'رقم زاگرس', 'رقم کرج', 'رقم ریجاب'],
ذرت: ['رقم سینگل کراس ۷۰۴', 'رقم سینگل کراس ۷۰۷', 'رقم ماکزیما'],
برنج: ['رقم فجر', 'رقم خزر', 'رقم طارم'],
پنبه: ['رقم ورامین', 'رقم ساحل', 'رقم سپید'],
'چغندر قند': ['رقم اکباتان', 'رقم شیرین', 'رقم پاییزه'],
سیبزمینی: ['رقم آگریا', 'رقم مارفونا', 'رقم ساوالان'],
گوجهفرنگی: ['رقم چری', 'رقم روتگرز', 'رقم پامیس'],
پیاز: ['رقم قرمز آذرشهر', 'رقم سفید قم', 'رقم زرد'],
سبزیجات: ['کاهو', 'جعفری', 'شوید', 'تره', 'ریحان']
}
const FormSensorHub = ({ onBack }: FormSensorHubProps) => { const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
const t = useTranslations('sensorHub') const t = useTranslations('sensorHub')
const [name, setName] = useState('') const [name, setName] = useState('')
const [uuidSensor, setUuidSensor] = useState('') const [uuidSensor, setUuidSensor] = useState('')
const [plantType, setPlantType] = useState<string | null>(null)
const [plantName, setPlantName] = useState<string | null>(null)
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
const [areaM2, setAreaM2] = useState<number | undefined>(undefined)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const handleAreaChange = useCallback((geojson: MapDrawGeoJSON, area?: number) => {
setAreaGeoJson(geojson)
setAreaM2(area)
}, [])
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError(null) setError(null)
setLoading(true) setLoading(true)
console.log(areaGeoJson)
try { try {
await sensorHubService.addSensor({ name, uuid_sensor: uuidSensor }) await sensorHubService.addSensor({
name,
uuid_sensor: uuidSensor,
...(areaGeoJson && { area_geojson: areaGeoJson }),
...(areaM2 !== undefined && { area_m2: areaM2 })
})
onBack() onBack()
} catch (err: unknown) { } catch (err: unknown) {
const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: string }).message) : t('errorSave') const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: string }).message) : t('errorSave')
@@ -83,6 +126,45 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
onChange={e => setUuidSensor(e.target.value)} onChange={e => setUuidSensor(e.target.value)}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CustomAutocomplete
fullWidth
options={PLANT_TYPES}
value={plantType}
onChange={(_, v) => {
setPlantType(v)
setPlantName(null)
}}
renderInput={params => <CustomTextField {...params} label={t('plantType')} />}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CustomAutocomplete
fullWidth
disabled={!plantType}
options={plantType ? PLANT_NAMES_BY_TYPE[plantType] ?? [] : []}
value={plantName}
onChange={(_, v) => setPlantName(v)}
renderInput={params => <CustomTextField {...params} label={t('plantName')} />}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<Typography variant='body2' color='text.secondary' className='mbe-2'>
{t('mapAreaDescription')}
</Typography>
<MapDraw
center={[35.6892, 51.389]}
zoom={13}
height={400}
singleShape
onAreaChange={handleAreaChange}
/>
{areaM2 !== undefined && areaM2 > 0 && (
<Typography variant='caption' color='text.secondary' className='mts-2 block'>
مساحت تقریبی: {areaM2 >= 10000 ? `${(areaM2 / 10000).toFixed(2)} هکتار` : `${areaM2.toFixed(0)} متر مربع`}
</Typography>
)}
</Grid>
<Grid size={{ xs: 12 }} className='flex gap-2'> <Grid size={{ xs: 12 }} className='flex gap-2'>
<Button variant='tonal' color='secondary' onClick={onBack} disabled={loading}> <Button variant='tonal' color='secondary' onClick={onBack} disabled={loading}>
{t('cancel')} {t('cancel')}