From bb83ab506e53a351fbafa42ba8359976512314f9 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Fri, 20 Feb 2026 23:08:44 +0330 Subject: [PATCH] Add weather information section and update dependencies - Introduced a new CropZoningWeatherSection component to display weather data in the crop zoning dashboard. - Updated package.json and package-lock.json to include chart.js and react-chartjs-2 for enhanced data visualization. - Added Persian translations for weather-related UI elements to support localization. --- messages/fa.json | 8 + package-lock.json | 30 ++ package.json | 2 + .../dashboard/plant-simulator/page.tsx | 7 + .../cropZoning/CropZoningWeatherSection.tsx | 170 +++++++ .../farm/cropZoning/CropZoningWrapper.tsx | 29 +- src/views/dashboards/farm/cropZoning/index.ts | 1 + .../farm/plantSimulator/PlantSimulator.tsx | 457 ++++++++++++++++++ 8 files changed, 692 insertions(+), 12 deletions(-) create mode 100644 src/app/(dashboard)/(private)/dashboard/plant-simulator/page.tsx create mode 100644 src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx create mode 100644 src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx diff --git a/messages/fa.json b/messages/fa.json index 102ce83..486f50a 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -552,6 +552,14 @@ "reason": "دلیل پیشنهاد", "criteriaChart": "نمودار تطابق معیارها", "changeCrop": "تغییر محصول" + }, + "weather": { + "title": "اطلاعات هواشناسی", + "temperature": "دما", + "humidity": "رطوبت", + "windSpeed": "سرعت باد", + "forecastChart": "پیش‌بینی هوا", + "forecastSubheader": "دما و رطوبت در ۷ روز آینده" } } } diff --git a/package-lock.json b/package-lock.json index 6fd05fb..3e6e073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@types/leaflet-draw": "^1.0.13", "apexcharts": "3.49.0", "bootstrap-icons": "1.11.3", + "chart.js": "^4.5.1", "classnames": "2.5.1", "cmdk": "1.0.4", "date-fns": "4.1.0", @@ -66,6 +67,7 @@ "next-intl": "3.25.2", "react": "18.3.1", "react-apexcharts": "1.4.1", + "react-chartjs-2": "^5.3.1", "react-colorful": "5.6.1", "react-date-object": "1.1.9", "react-datepicker": "7.3.0", @@ -1707,6 +1709,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://mirror-npm.runflare.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mapbox/geojson-area": { "version": "0.2.2", "resolved": "https://mirror-npm.runflare.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz", @@ -4566,6 +4574,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://mirror-npm.runflare.com/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/cheap-ruler": { "version": "4.0.0", "license": "ISC" @@ -8952,6 +8972,16 @@ "react": ">=0.13" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://mirror-npm.runflare.com/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-colorful": { "version": "5.6.1", "license": "MIT", diff --git a/package.json b/package.json index 19faa84..f3aca02 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/leaflet-draw": "^1.0.13", "apexcharts": "3.49.0", "bootstrap-icons": "1.11.3", + "chart.js": "^4.5.1", "classnames": "2.5.1", "cmdk": "1.0.4", "date-fns": "4.1.0", @@ -71,6 +72,7 @@ "next-intl": "3.25.2", "react": "18.3.1", "react-apexcharts": "1.4.1", + "react-chartjs-2": "^5.3.1", "react-colorful": "5.6.1", "react-date-object": "1.1.9", "react-datepicker": "7.3.0", diff --git a/src/app/(dashboard)/(private)/dashboard/plant-simulator/page.tsx b/src/app/(dashboard)/(private)/dashboard/plant-simulator/page.tsx new file mode 100644 index 0000000..bae4823 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/plant-simulator/page.tsx @@ -0,0 +1,7 @@ +import PlantSimulator from '@views/dashboards/farm/plantSimulator/PlantSimulator' + +const PlantSimulatorPage = () => { + return +} + +export default PlantSimulatorPage diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx new file mode 100644 index 0000000..445496f --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/CropZoningWeatherSection.tsx @@ -0,0 +1,170 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useTranslations } from 'next-intl' +import dynamic from 'next/dynamic' + +// MUI Imports +import Box from '@mui/material/Box' +import Grid from '@mui/material/Grid2' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Component Imports +import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard' +import { farmDashboardService } from '@/libs/api/services/farmDashboardService' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +const DEFAULT_WEATHER = { + temperature: 24, + condition: 'آفتابی', + humidity: 45, + windSpeed: 12, + windUnit: 'km/h', + unit: '°C', + precipitation: 0 +} + +const DEFAULT_FORECAST_SERIES = [ + { name: 'دما', data: [18, 22, 26, 28, 25, 20, 18] }, + { name: 'رطوبت', data: [55, 48, 42, 38, 45, 52, 58] } +] + +const FORECAST_CATEGORIES = ['امروز', 'فردا', 'شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه'] + +export default function CropZoningWeatherSection() { + const t = useTranslations('cropZoning.weather') + const [weatherData, setWeatherData] = useState>(DEFAULT_WEATHER) + const [forecastSeries, setForecastSeries] = useState(DEFAULT_FORECAST_SERIES) + + useEffect(() => { + farmDashboardService + .getAllCards() + .then(cards => { + const w = cards?.farmWeatherCard + if (w && typeof w === 'object') { + setWeatherData({ ...DEFAULT_WEATHER, ...w }) + const chartData = w.chartData as { labels?: string[]; series?: number[][] } | undefined + if (chartData?.series?.[0]) { + setForecastSeries([{ name: 'دما', data: chartData.series[0] }]) + } + } + }) + .catch(() => {}) + }, []) + + const forecastOptions: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { show: false }, + zoom: { enabled: false } + }, + colors: ['var(--mui-palette-info-main)', 'var(--mui-palette-success-main)'], + stroke: { width: 2, curve: 'smooth' }, + legend: { + position: 'top', + labels: { colors: 'var(--mui-palette-text-secondary)' } + }, + dataLabels: { enabled: false }, + grid: { + borderColor: 'var(--mui-palette-divider)', + strokeDashArray: 4, + xaxis: { lines: { show: false } }, + yaxis: { lines: { show: true } } + }, + xaxis: { + categories: FORECAST_CATEGORIES, + labels: { style: { colors: 'var(--mui-palette-text-disabled)' } }, + axisBorder: { show: false }, + axisTicks: { show: false } + }, + yaxis: { + labels: { + style: { colors: 'var(--mui-palette-text-disabled)' } + } + } + } + + const temp = (weatherData.temperature as number) ?? 24 + const humidity = (weatherData.humidity as number) ?? 45 + const windSpeed = (weatherData.windSpeed as number) ?? 12 + + return ( + + + {t('title')} + + + + + + + + + + + + {t('temperature')} + + + {temp} + {weatherData.unit ?? '°C'} + + + + + + + + + + + {t('humidity')} + + {humidity}% + + + + + + + + + + {t('windSpeed')} + + + {windSpeed} {(weatherData.windUnit as string) ?? 'km/h'} + + + + + + + + + + + + + + + + ) +} diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx index 26ba89f..d571ca9 100644 --- a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx +++ b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx @@ -10,6 +10,7 @@ import CropZoningMap from './CropZoningMap' import ZoneLegend from './ZoneLegend' import LayerControl from './LayerControl' import ZoneDetailPanel from './ZoneDetailPanel' +import CropZoningWeatherSection from './CropZoningWeatherSection' import type { LayerType } from './cropZoningTypes' import type { ZoneFeatureProperties } from './cropZoningTypes' import type { MapDrawGeoJSON } from './CropZoningMap' @@ -45,18 +46,19 @@ export default function CropZoningWrapper() { }, []) return ( - - - + + + + @@ -84,6 +86,9 @@ export default function CropZoningWrapper() { onClose={() => setPanelOpen(false)} zone={selectedZone} /> + + + ) } diff --git a/src/views/dashboards/farm/cropZoning/index.ts b/src/views/dashboards/farm/cropZoning/index.ts index 52dd641..e3eca74 100644 --- a/src/views/dashboards/farm/cropZoning/index.ts +++ b/src/views/dashboards/farm/cropZoning/index.ts @@ -1,4 +1,5 @@ export { default as CropZoningWrapper } from './CropZoningWrapper' +export { default as CropZoningWeatherSection } from './CropZoningWeatherSection' export { default as CropZoningMap } from './CropZoningMap' export { default as ZoneLegend } from './ZoneLegend' export { default as LayerControl } from './LayerControl' diff --git a/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx b/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx new file mode 100644 index 0000000..34c3635 --- /dev/null +++ b/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx @@ -0,0 +1,457 @@ +'use client' + +import { useEffect, useRef, useState, useCallback } from 'react' +import { + Chart as ChartJS, + LineElement, + PointElement, + LinearScale, + CategoryScale, + Title, + Tooltip, + Legend, + Filler +} from 'chart.js' +import { Line } from 'react-chartjs-2' + +ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend, Filler) + +// ─── Types ─────────────────────────────────────────────────────────────────── + +interface Leaf { + id: number + side: 'left' | 'right' + heightFraction: number // 0..1 relative to stem + angle: number // base rotation angle in degrees + scale: number // 0..1 grow-in scale + swayOffset: number // random sway phase offset +} + +interface PlantState { + height: number // current stem height px (0 → MAX_HEIGHT) + leaves: Leaf[] + tick: number +} + +interface EnvironmentSettings { + light: number // 0..100 + water: number // 0..100 +} + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const MAX_HEIGHT = 260 +const SVG_WIDTH = 200 +const SVG_HEIGHT = 320 +const STEM_X = SVG_WIDTH / 2 +const BASE_Y = SVG_HEIGHT - 10 +const LEAF_INTERVAL_PX = 36 // stem height growth between new leaves +const MAX_LEAVES = 12 + +// ─── Helper: compute growth speed multiplier from env settings ──────────────── + +function growthRate(env: EnvironmentSettings, speed: number): number { + const lightFactor = 0.3 + (env.light / 100) * 0.7 + const waterFactor = 0.3 + (env.water / 100) * 0.7 + return speed * lightFactor * waterFactor +} + +// ─── Plant SVG Component ────────────────────────────────────────────────────── + +function PlantSVG({ plant, tick }: { plant: PlantState; tick: number }) { + const stemTop = BASE_Y - plant.height + const stemHeight = plant.height + + return ( + + {/* Soil */} + + + {/* Stem */} + {stemHeight > 0 && ( + + )} + + {/* Leaves */} + {plant.leaves.map(leaf => { + const leafY = BASE_Y - leaf.heightFraction * plant.height + const swayAngle = Math.sin((tick / 18 + leaf.swayOffset) * 1) * 4 + const baseAngle = leaf.side === 'left' ? -30 - swayAngle : 30 + swayAngle + + return ( + + + + ) + })} + + {/* Bud / flower at the top */} + {stemHeight > MAX_HEIGHT * 0.85 && ( + + + + + )} + + ) +} + +// ─── Growth Chart ───────────────────────────────────────────────────────────── + +function GrowthChart({ + heightHistory, + leafHistory +}: { + heightHistory: number[] + leafHistory: number[] +}) { + const labels = heightHistory.map((_, i) => `${i}s`) + + const data = { + labels, + datasets: [ + { + label: 'ارتفاع (px)', + data: heightHistory, + borderColor: '#4a7c59', + backgroundColor: 'rgba(74,124,89,0.15)', + fill: true, + tension: 0.4, + pointRadius: 0, + yAxisID: 'yHeight' + }, + { + label: 'تعداد برگ', + data: leafHistory, + borderColor: '#f9c74f', + backgroundColor: 'rgba(249,199,79,0.15)', + fill: true, + tension: 0.4, + pointRadius: 0, + yAxisID: 'yLeaf' + } + ] + } + + const options = { + responsive: true, + animation: { duration: 0 }, + plugins: { + legend: { labels: { color: '#e2e8f0', font: { size: 12 } } }, + title: { + display: true, + text: 'نمودار رشد گیاه', + color: '#e2e8f0', + font: { size: 14 } + } + }, + scales: { + x: { + ticks: { color: '#94a3b8', maxTicksLimit: 8 }, + grid: { color: 'rgba(148,163,184,0.1)' } + }, + yHeight: { + type: 'linear' as const, + position: 'left' as const, + min: 0, + max: MAX_HEIGHT, + ticks: { color: '#4a7c59' }, + grid: { color: 'rgba(148,163,184,0.1)' }, + title: { display: true, text: 'ارتفاع', color: '#4a7c59' } + }, + yLeaf: { + type: 'linear' as const, + position: 'right' as const, + min: 0, + max: MAX_LEAVES, + ticks: { color: '#f9c74f' }, + grid: { display: false }, + title: { display: true, text: 'برگ', color: '#f9c74f' } + } + } + } + + return +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export default function PlantSimulator() { + const [running, setRunning] = useState(false) + const [speed, setSpeed] = useState(1.5) // px per tick base + const [env, setEnv] = useState({ light: 75, water: 65 }) + + const [plant, setPlant] = useState({ height: 0, leaves: [], tick: 0 }) + const [heightHistory, setHeightHistory] = useState([0]) + const [leafHistory, setLeafHistory] = useState([0]) + + const intervalRef = useRef | null>(null) + const tickRef = useRef(0) + const historyIntervalRef = useRef | null>(null) + + const reset = useCallback(() => { + setPlant({ height: 0, leaves: [], tick: 0 }) + setHeightHistory([0]) + setLeafHistory([0]) + tickRef.current = 0 + }, []) + + // Main simulation tick at ~30fps + useEffect(() => { + if (!running) { + if (intervalRef.current) clearInterval(intervalRef.current) + return + } + + intervalRef.current = setInterval(() => { + tickRef.current += 1 + const t = tickRef.current + + setPlant(prev => { + const rate = growthRate(env, speed) + const newHeight = Math.min(prev.height + rate * 0.4, MAX_HEIGHT) + const newLeaves = [...prev.leaves] + + // Add a new leaf every LEAF_INTERVAL_PX of stem growth + const expectedLeaves = Math.min(Math.floor(newHeight / LEAF_INTERVAL_PX), MAX_LEAVES) + + while (newLeaves.length < expectedLeaves) { + const id = newLeaves.length + newLeaves.push({ + id, + side: id % 2 === 0 ? 'left' : 'right', + heightFraction: (id + 1) / (expectedLeaves + 1), + angle: 0, + scale: 0, + swayOffset: Math.random() * Math.PI * 2 + }) + } + + // Grow existing leaves (scale 0 → 1 over ~40 ticks) + const grownLeaves = newLeaves.map(l => ({ + ...l, + scale: Math.min(l.scale + 0.025, 1) + })) + + return { height: newHeight, leaves: grownLeaves, tick: t } + }) + }, 33) + + return () => { + if (intervalRef.current) clearInterval(intervalRef.current) + } + }, [running, speed, env]) + + // History logging every 1 second + useEffect(() => { + if (!running) { + if (historyIntervalRef.current) clearInterval(historyIntervalRef.current) + return + } + + historyIntervalRef.current = setInterval(() => { + setPlant(prev => { + setHeightHistory(h => [...h.slice(-59), Math.round(prev.height)]) + setLeafHistory(l => [...l.slice(-59), prev.leaves.length]) + return prev + }) + }, 1000) + + return () => { + if (historyIntervalRef.current) clearInterval(historyIntervalRef.current) + } + }, [running]) + + const isFinished = plant.height >= MAX_HEIGHT + + return ( +
+

+ 🌱 شبیه‌ساز رشد گیاه +

+ +
+ + {/* ── Left: Plant visualization ── */} +
+
+ + + {/* Stats */} +
+
+
{Math.round(plant.height)}
+
ارتفاع (px)
+
+
+
{plant.leaves.length}
+
تعداد برگ
+
+
+ + {isFinished && ( +
+ 🌼 گیاه به حداکثر رشد رسید! +
+ )} +
+ + {/* Controls */} +
+

کنترل‌ها

+ + {/* Start / Stop / Reset */} +
+ + +
+ + {/* Speed slider */} +
+ + setSpeed(Number(e.target.value))} + className='w-full accent-green-500' + /> +
+ + {/* Light */} +
+ + setEnv(prev => ({ ...prev, light: Number(e.target.value) }))} + className='w-full accent-yellow-400' + /> +
+ + {/* Water */} +
+ + setEnv(prev => ({ ...prev, water: Number(e.target.value) }))} + className='w-full accent-blue-400' + /> +
+ + {/* Effective rate indicator */} +
+ نرخ رشد مؤثر:{' '} + + {growthRate(env, speed).toFixed(2)}× + +
+
+
+ + {/* ── Right: Chart ── */} +
+ + + {/* Info cards */} +
+
+
پیشرفت رشد
+
+
+
+
+ {Math.round((plant.height / MAX_HEIGHT) * 100)}% +
+
+ +
+
وضعیت نور
+
+
+
+
{env.light}%
+
+ +
+
وضعیت آب
+
+
+
+
{env.water}%
+
+
+ + {/* Description */} +
+

+ این شبیه‌ساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی + محاسبه می‌کند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی + در باد نمایش داده می‌شود. نمودار تغییرات ارتفاع و تعداد برگ‌ها را در طول + زمان ثبت می‌کند. +

+
+
+ +
+
+ ) +}