From a9a996e1e84ef4b95b608cfbe4796991d22d899f Mon Sep 17 00:00:00 2001 From: mhmjalali Date: Sun, 14 Jun 2026 09:49:13 +0330 Subject: [PATCH] add map to new mission on rowAction of operators --- .../Dialog/Form/Area/Routing/AlarmText.jsx | 28 +++ .../Dialog/Form/Area/Routing/DataBox.jsx | 53 +++++ .../Dialog/Form/Area/Routing/ShowRoute.jsx | 46 ++++ .../Dialog/Form/Area/Routing/index.jsx | 219 ++++++++++++++++++ .../AddController/Dialog/Form/Area/index.jsx | 34 +-- .../Dialog/Form/Verify/index.jsx | 8 +- .../Add/AddController/Dialog/Form/index.jsx | 3 +- .../Add/AddController/Dialog/index.jsx | 23 +- 8 files changed, 368 insertions(+), 46 deletions(-) create mode 100644 src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/AlarmText.jsx create mode 100644 src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/DataBox.jsx create mode 100644 src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/ShowRoute.jsx create mode 100644 src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/index.jsx diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/AlarmText.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/AlarmText.jsx new file mode 100644 index 0000000..0a3be1b --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/AlarmText.jsx @@ -0,0 +1,28 @@ +import { Box, Typography } from "@mui/material"; + +const AlarmText = ({ isSource }) => { + if (isSource) return null; + return ( + + + با کلیک بر روی مارکر موقعیت را انتخاب کنید + + + ); +}; + +export default AlarmText; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/DataBox.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/DataBox.jsx new file mode 100644 index 0000000..c19cb6f --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/DataBox.jsx @@ -0,0 +1,53 @@ +import { Box, Button, TextField } from "@mui/material"; + +const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { + if (!isSource) return null; + + return ( + +
+ + {isSource && isDest && ( + + )} +
+ {isDest && ( +
+ + +
+ )} +
+ ); +}; + +export default DataBox; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/ShowRoute.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/ShowRoute.jsx new file mode 100644 index 0000000..5e1a4bd --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/ShowRoute.jsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from "react"; +import { Marker, Polyline, useMap } from "react-leaflet"; +import L from "leaflet"; +import polyline from "@mapbox/polyline"; +import SourceIcon from "@/assets/images/source-icon.svg"; +import DestIcon from "@/assets/images/destination-icon.svg"; + +const ShowRoute = ({ area }) => { + const map = useMap(); + const defaultIconSize = [35, 35]; + + const createCustomIcon = (size, iconUrl) => { + return L.icon({ + iconUrl: iconUrl, + iconSize: size, + iconAnchor: [size[0] / 2, size[1]], + popupAnchor: [0, -size[1]], + }); + }; + + useEffect(() => { + if (coords.length > 0) { + map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 }); + } + }, [area]); + const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); + const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); + + if (!area) return null; + const coords = polyline.decode(area, 6); + + if (coords.length === 0) return null; + + const sourcePosition = coords[0]; + const destPosition = coords[coords.length - 1]; + + return ( + <> + + + + + ); +}; + +export default ShowRoute; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/index.jsx new file mode 100644 index 0000000..06e7c9b --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/Routing/index.jsx @@ -0,0 +1,219 @@ +import { Marker, Polyline, useMapEvents } from "react-leaflet"; +import L from "leaflet"; +import polyline from "@mapbox/polyline"; +import SourceIcon from "@/assets/images/source-icon.svg"; +import PrevSourceIcon from "@/assets/images/prev-source-icon.svg"; +import PrevDestIcon from "@/assets/images/prev-destination-icon.svg"; +import DestIcon from "@/assets/images/destination-icon.svg"; +import { useEffect, useRef, useState } from "react"; +import DataBox from "./DataBox"; +import AlarmText from "./AlarmText"; + +const Routing = ({ setArea, setLocations, locations, area }) => { + const mapPrevSourceMarker = useRef(); + const mapPrevDestMarker = useRef(); + const mapRef = useRef(null); + const defaultIconSize = [35, 35]; + const [isSource, setIsSource] = useState(false); + const [sourcePosition, setSourcePosition] = useState(null); + const [isDest, setIsDest] = useState(false); + const [destPosition, setDestPosition] = useState(null); + const [routeCoords, setRouteCoords] = useState([]); + const [alternateCoords, setAlternateCoords] = useState([]); + const [editingTarget, setEditingTarget] = useState(null); + const [selectedRoute, setSelectedRoute] = useState(null); + + const createCustomIcon = (size, iconUrl) => { + return L.icon({ + iconUrl: iconUrl, + iconSize: size, + iconAnchor: [size[0] / 2, size[1]], + popupAnchor: [0, -size[1]], + }); + }; + + const defaultIcon = createCustomIcon( + defaultIconSize, + isSource && editingTarget !== "source" ? SourceIcon.src : PrevSourceIcon.src + ); + + const prevDestIcon = createCustomIcon( + defaultIconSize, + isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src + ); + + const isMovingSource = !isSource || editingTarget === "source"; + const isMovingDest = isSource && (!isDest || editingTarget === "dest"); + + const map = useMapEvents({ + move(e) { + if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); + if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); + }, + movestart() { + if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); + if (isMovingDest && mapPrevDestMarker.current) + mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + }, + moveend() { + if (isMovingSource) + mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); + if (isMovingDest && mapPrevDestMarker.current) + mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + }, + }); + + useEffect(() => { + mapRef.current = map; + }, [map]); + + useEffect(() => { + if (locations.length < 2) return; + const restoredSource = { lat: locations[0].lat, lng: locations[0].lon }; + const restoredDest = { lat: locations[1].lat, lng: locations[1].lon }; + setSourcePosition(restoredSource); + setDestPosition(restoredDest); + setIsSource(true); + setIsDest(true); + if (area) { + const decoded = polyline.decode(area, 6); + setRouteCoords(decoded); + setSelectedRoute({ index: "main", coords: decoded }); + setTimeout(() => { + mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); + }, 100); + } else { + fetchRoute(restoredSource, restoredDest); + } + }, []); + + const fetchRoute = async (source, dest) => { + const locs = [ + { lat: source.lat, lon: source.lng }, + { lat: dest.lat, lon: dest.lng }, + ]; + try { + const response = await fetch("https://jouya.141.ir/api/router/drive", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ locations: locs }), + }); + const data = await response.json(); + const shape = data.trip.legs[0].shape; + setLocations(locs); + setArea(shape); + const decoded = polyline.decode(shape, 6); + setRouteCoords(decoded); + setSelectedRoute({ index: "main", coords: decoded }); + + if (data.alternates?.length > 0) { + const decodedAlternates = data.alternates.map((alt) => polyline.decode(alt.trip.legs[0].shape, 6)); + setAlternateCoords(decodedAlternates); + } else { + setAlternateCoords([]); + } + map.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); + } catch (error) { + console.error("Routing error:", error); + } + }; + + const handleMarkerClick = async () => { + if (!isSource || editingTarget === "source") { + const clickedPosition = mapPrevSourceMarker.current.getLatLng(); + setSourcePosition(clickedPosition); + setIsSource(true); + setEditingTarget(null); + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + + if (editingTarget === "source" && destPosition) { + await fetchRoute(clickedPosition, destPosition); + } + } + }; + + const handleDestMarkerClick = async () => { + if (!isDest || editingTarget === "dest") { + const clickedPosition = mapPrevDestMarker.current.getLatLng(); + setDestPosition(clickedPosition); + setIsDest(true); + setEditingTarget(null); + + await fetchRoute(sourcePosition, clickedPosition); + } + }; + + const handleEditSource = () => { + setEditingTarget("source"); + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + }; + + const handleEditDest = () => { + setEditingTarget("dest"); + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + }; + + const handleSelectRoute = (index, coords) => { + setSelectedRoute({ index, coords }); + const encoded = polyline.encode(coords, 6); + setArea(encoded); + map.flyToBounds(coords, { padding: [50, 50], duration: 0.3 }); + }; + + return ( + <> + + + + {isSource && ( + + )} + {alternateCoords.map((coords, index) => ( + handleSelectRoute(index, coords) }} + /> + ))} + {routeCoords.length > 0 && ( + handleSelectRoute("main", routeCoords) }} + /> + )} + + ); +}; + +export default Routing; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/index.jsx index c8feef7..200c1e0 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Area/index.jsx @@ -2,41 +2,29 @@ import MapLoading from "@/core/components/MapLayer/Loading"; import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material"; import dynamic from "next/dynamic"; import { useCallback, useState } from "react"; -import MapControlPolygon from "./MapControlPolygon"; -import MapControlPolyline from "./MapControlPolyline"; -import SelectBoundType from "./SelectBoundType"; -import OldBoundLayer from "./OldBoundLayer"; +import Routing from "./Routing"; const MapLayer = dynamic(() => import("@/core/components/MapLayer"), { loading: () => , ssr: false, }); -const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => { - const [bound, setBound] = useState(allData.bound); - const [boundType, setBoundType] = useState(allData.bound_type); +const Area = ({ allData, setAllData, handlePrev, setTabState }) => { + const [area, setArea] = useState(allData.area); + const [locations, setLocations] = useState(allData.locations); const handleNext = useCallback(() => { - setAllData({ bound: bound }); + setAllData({ area: area }); + setAllData({ locations: locations }); setTabState((s) => s + 1); - }, [bound]); + }, [area]); + return ( <> - - {boundType == "polygon" ? ( - - ) : ( - - )} - + @@ -45,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => { - diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Verify/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Verify/index.jsx index bf57f0b..89e0b41 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Verify/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/Verify/index.jsx @@ -5,7 +5,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr import moment from "jalali-moment"; import dynamic from "next/dynamic"; import { useCallback } from "react"; -import ShowBound from "../../../../../../Actions/showBound"; +import ShowRoute from "../Area/Routing/ShowRoute"; const MapLayer = dynamic(() => import("@/core/components/MapLayer"), { loading: () => , ssr: false, @@ -23,7 +23,7 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => { - + @@ -93,11 +93,11 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => { label={`${allData.machine.machine_code} | ${allData.machine.car_name}`} /> - + {/* راننده - + */} diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/index.jsx index 054e6e6..0adbb1f 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/index.jsx @@ -25,6 +25,8 @@ const reducer = (state, action) => { }; const CreateForm = ({ defaultValues, submitForm, setOpen, submitting, oldBound }) => { + console.log("defaultValues", defaultValues); + const [allData, dispatch] = useReducer(reducer, defaultValues); const [tabState, setTabState] = useState(0); const handleClose = () => { @@ -84,7 +86,6 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting, oldBound } { dispatch({ type: "changeData", data }); diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/index.jsx index 3c0f5da..31c0005 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/index.jsx @@ -10,22 +10,12 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { const submitForm = async (result) => { setSubmitting(true); - const bound = result.bound.getLatLngs(); - let area = - result.bound_type === "polygon" - ? bound.map((ring) => ring.map((latlng) => [latlng.lng, latlng.lat]))[0] - : bound.map((latlng) => [latlng.lng, latlng.lat]); - - // بستن polygon - if (result.bound_type === "polygon" && area.length > 0) { - const firstPoint = area[0]; - area.push({ ...firstPoint }); - } await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", { data: { ...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}), machine_id: result.machine.id, - driver: result.driver.id, + encoded_route: result.area, + driver: row.original.driver_id, zone: result.region, type: result.type, ...(result.type == 1 @@ -36,10 +26,6 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { end_date: result.end_date, }), end_point: result.end_point, - area: { - type: result.bound_type, - coordinates: area, - }, category_id: result.category_id, explanation: result.explanation, ...(result.category_id == 3 @@ -65,7 +51,6 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { defaultValues={{ explanation: "", category_id: "", - bound: null, rahdaran: rahdaran.filter((r) => !r.is_driver), bound_type: "polyline", type: "", @@ -74,7 +59,9 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { end_point: "", region: "", machine: machine, - driver: rahdaran.find((r) => r.is_driver), + driver: row.original.driver_id, + area: null, + locations: [], }} oldBound={oldBound} submitForm={submitForm} -- 2.49.1