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:
File diff suppressed because it is too large
Load Diff
Vendored
+2
@@ -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'
|
||||||
|
|||||||
@@ -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": "خطا در ذخیره سنسور",
|
||||||
|
|||||||
Generated
+91
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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: 'فروشگاه',
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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')}
|
||||||
|
|||||||
Reference in New Issue
Block a user