diff --git a/.env.example b/.env.example index 5dd7f8a..e452915 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ HOST="rms.witel.ir" HOST_RMTO="rms.rmto.ir" -NEXT_PUBLIC_VERSION="1.9.0" +NEXT_PUBLIC_VERSION="1.9.1" NEXT_PUBLIC_API_URL="https://rms.witel.ir" NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map" \ No newline at end of file diff --git a/src/assets/images/prev-waypoint-icon.svg b/src/assets/images/prev-waypoint-icon.svg new file mode 100644 index 0000000..7c8c128 --- /dev/null +++ b/src/assets/images/prev-waypoint-icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/waypoint-icon.svg b/src/assets/images/waypoint-icon.svg new file mode 100644 index 0000000..572cee1 --- /dev/null +++ b/src/assets/images/waypoint-icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/DataBox.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/DataBox.jsx index 5768f75..3253145 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/DataBox.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/DataBox.jsx @@ -1,50 +1,135 @@ -import { Box, Button, TextField } from "@mui/material"; +import { Box, Stack, IconButton, Typography, Divider } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; -const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { +const ROW_COLORS = { + source: "#0EA37A", // سبز + waypoint: "#F5A623", // زرد/نارنجی + dest: "#E53935", // قرمز +}; + +const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null); + +const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => ( + + + + + {label} + + + {value || placeholder} + + + {onEdit && ( + + + + )} + {onRemove && ( + + + + )} + +); + +const DataBox = ({ + isSource, + isDest, + sourcePosition, + destPosition, + editingTarget, + onEditSource, + onEditDest, + waypoints = [], + onEditWaypoint, + onAddWaypoint, + onRemoveWaypoint, + canAddWaypoint, +}) => { if (!isSource) return null; return ( -
- + + {waypoints.map((wp, index) => ( + onEditWaypoint(index) : null} + onRemove={() => onRemoveWaypoint(index)} /> - {isSource && isDest && ( - - )} -
+ ))} + {isDest && ( -
- - -
+ + )} + + {canAddWaypoint && ( + <> + + + + + افزودن نقطه میانی + + + )}
); diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/ShowRoute.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/ShowRoute.jsx index b05cc6a..b9d5143 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/ShowRoute.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/ShowRoute.jsx @@ -55,3 +55,74 @@ const ShowRoute = ({ area }) => { }; export default ShowRoute; + +// import { useEffect, useState } 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"; +// import WaypointIcon from "@/assets/images/waypoint-icon.svg"; +// +// 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]], +// }); +// }; +// +// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); +// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); +// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src); +// +// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده +// const ShowRoute = ({ area, locations = [] }) => { +// const map = useMap(); +// const [showRoute, setShowRoute] = useState(false); +// +// const coords = area ? polyline.decode(area, 6) : []; +// +// useEffect(() => { +// if (!coords.length) return; +// +// setShowRoute(false); +// +// map.flyToBounds(coords, { +// padding: [50, 50], +// duration: 1.5, +// }); +// +// map.once("moveend", () => { +// setShowRoute(true); +// }); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [area]); +// +// if (!area || coords.length === 0) return null; +// +// const sourcePosition = coords[0]; +// const destPosition = coords[coords.length - 1]; +// +// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری) +// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : []; +// +// return ( +// <> +// {showRoute && } +// +// +// +// {middleWaypoints.map((wp, index) => ( +// +// ))} +// +// +// +// ); +// }; +// +// export default ShowRoute; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/index.jsx index 49c4651..6d7fc4e 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/Area/Routing/index.jsx @@ -5,19 +5,29 @@ 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 WaypointIcon from "@/assets/images/prev-waypoint-icon.svg"; +import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg"; import { useEffect, useRef, useState } from "react"; import DataBox from "./DataBox"; import AlarmText from "./AlarmText"; +const MAX_WAYPOINTS = 2; + const Routing = ({ setArea, setLocations, locations, area }) => { const mapPrevSourceMarker = useRef(); const mapPrevDestMarker = useRef(); + const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint 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 [waypoints, setWaypoints] = useState([]); + const [routeCoords, setRouteCoords] = useState([]); const [alternateCoords, setAlternateCoords] = useState([]); const [editingTarget, setEditingTarget] = useState(null); @@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => { isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src ); - const isMovingSource = !isSource || editingTarget === "source"; - const isMovingDest = isSource && (!isDest || editingTarget === "dest"); + const getWaypointIcon = (index) => { + const wp = waypoints[index]; + const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`; + return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src); + }; + + const getActiveTarget = () => { + if (editingTarget) return editingTarget; + if (!isSource) return "source"; + const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed); + if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`; + if (!isDest) return "dest"; + return null; + }; const map = useMapEvents({ move(e) { - if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); - if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); + const target = getActiveTarget(); + const center = e.target.getCenter(); + if (target === "source") { + mapPrevSourceMarker.current?.setLatLng(center); + } else if (target === "dest") { + mapPrevDestMarker.current?.setLatLng(center); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setLatLng(center); + } }, movestart() { - if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src)); + } }, moveend() { - if (isMovingSource) - mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src)); + } }, }); @@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => { 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); + const first = { lat: locations[0].lat, lng: locations[0].lon }; + const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon }; + const middle = locations.slice(1, -1).map((loc) => ({ + position: { lat: loc.lat, lng: loc.lon }, + confirmed: true, + })); + + setSourcePosition(first); + setDestPosition(last); + setWaypoints(middle); setIsSource(true); setIsDest(true); + if (area) { const decoded = polyline.decode(area, 6); setRouteCoords(decoded); @@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => { mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); }, 100); } else { - fetchRoute(restoredSource, restoredDest); + fetchRoute(first, last, middle); } }, []); - const fetchRoute = async (source, dest) => { + const fetchRoute = async (source, dest, waypointsList = waypoints) => { + const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position); const locs = [ { lat: source.lat, lon: source.lng }, + ...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })), { lat: dest.lat, lon: dest.lng }, ]; try { @@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => { } }; + const resetRoute = () => { + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + }; + const handleMarkerClick = async () => { if (!isSource || editingTarget === "source") { const clickedPosition = mapPrevSourceMarker.current.getLatLng(); setSourcePosition(clickedPosition); setIsSource(true); + const wasEditing = editingTarget === "source"; setEditingTarget(null); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); - if (editingTarget === "source" && destPosition) { - await fetchRoute(clickedPosition, destPosition); + if (wasEditing && destPosition) { + await fetchRoute(clickedPosition, destPosition, waypoints); } } }; @@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => { setDestPosition(clickedPosition); setIsDest(true); setEditingTarget(null); + resetRoute(); - await fetchRoute(sourcePosition, clickedPosition); + await fetchRoute(sourcePosition, clickedPosition, waypoints); + } + }; + + const handleWaypointMarkerClick = async (index) => { + const wp = waypoints[index]; + if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت) + + const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng(); + const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w)); + setWaypoints(updated); + const wasEditing = editingTarget === `waypoint-${index}`; + setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); } }; const handleEditSource = () => { setEditingTarget("source"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); }; const handleEditDest = () => { setEditingTarget("dest"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); + }; + + const handleEditWaypoint = (index) => { + // برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه + setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w))); + setEditingTarget(`waypoint-${index}`); + resetRoute(); + }; + + // اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS) + const handleAddWaypoint = () => { + if (waypoints.length >= MAX_WAYPOINTS) return; + if (!isSource) return; // قبل از انتخاب مبدا معنی نداره + setWaypoints((prev) => [...prev, { position: null, confirmed: false }]); + setEditingTarget(`waypoint-${waypoints.length}`); + resetRoute(); + }; + + // حذف یک نقطه میانی + const handleRemoveWaypoint = async (index) => { + const updated = waypoints.filter((_, i) => i !== index); + setWaypoints(updated); + mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index); + if (editingTarget === `waypoint-${index}`) setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); + } }; const handleSelectRoute = (index, coords) => { @@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => { editingTarget={editingTarget} onEditSource={handleEditSource} onEditDest={handleEditDest} + waypoints={waypoints} + onEditWaypoint={handleEditWaypoint} + onAddWaypoint={handleAddWaypoint} + onRemoveWaypoint={handleRemoveWaypoint} + canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS} /> + + + {waypoints.map((wp, index) => ( + (mapPrevWaypointMarkers.current[index] = el)} + eventHandlers={{ click: () => handleWaypointMarkerClick(index) }} + icon={getWaypointIcon(index)} + /> + ))} + {isSource && ( { icon={prevDestIcon} /> )} + {alternateCoords.map((coords, index) => ( { + const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems(); + function getItemName(val) { + return itemsList.find((item) => item.id === val)?.name; + } + + return ( + ( + { + field.onChange(value); + setValue("explanation", getItemName(value)); + setValue("sub_item_id", ""); + }} + /> + )} + /> + ); +}; +export default ItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/SubItemSelectField.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/SubItemSelectField.jsx new file mode 100644 index 0000000..9d54a52 --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/SubItemSelectField.jsx @@ -0,0 +1,37 @@ +import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems"; +import { Controller } from "react-hook-form"; +import SelectBox from "@/core/components/SelectBox"; + +const SubItemSelectField = ({ control, watch, setValue, getValues }) => { + const itemId = watch("item_id"); + const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId); + + function createExplanation(val) { + const currentExplanation = getValues("explanation"); + const selectedItem = subItemsList.find((item) => item.id === val)?.name; + return `${currentExplanation}-${selectedItem}`; + } + + return ( + ( + { + field.onChange(val); + setValue("explanation", createExplanation(val)); + }} + /> + )} + /> + ); +}; +export default SubItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/index.jsx index f4e66e5..1a804de 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/Form/GetItemInfo/index.jsx @@ -9,12 +9,15 @@ import { ExitToApp } from "@mui/icons-material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Controller, useForm } from "react-hook-form"; import { array, object, string } from "yup"; +import ItemSelectField from "./ItemSelectField"; +import SubItemSelectField from "./SubItemSelectField"; const validationSchema = object({ - explanation: string().required("موضوع را مشخص کنید!"), end_point: string().required("مقصد را مشخص کنید!"), region: string().required("محور ماموریت را مشخص کنید!"), category_id: string().required("نوع ماموریت را مشخص کنید!"), + item_id: string().required("موضوع را مشخص کنید!"), + sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"), requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"), road_observed_id: string().when("category_id", { is: "3", @@ -25,6 +28,8 @@ const validationSchema = object({ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const defaultValues = { + item_id: allData.item_id, + sub_item_id: allData.sub_item_id, explanation: allData.explanation, end_point: allData.end_point, region: allData.region, @@ -33,7 +38,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { road_observed_id: allData.road_observed_id, }; - const { control, handleSubmit } = useForm({ + const { control, handleSubmit, watch, setValue, getValues } = useForm({ defaultValues, resolver: yupResolver(validationSchema), mode: "all", @@ -51,24 +56,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { - + + + ( - - )} + watch={watch} + setValue={setValue} + getValues={getValues} /> diff --git a/src/components/dashboard/roadMissions/operator/Actions/Create/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Create/index.jsx index 2595dca..054de48 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Create/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Create/index.jsx @@ -44,6 +44,8 @@ const Create = ({ mutate }) => { start_date: result.start_date, end_date: result.end_date, }), + item_id: result.item_id, + sub_item_id: result.sub_item_id, }, hasSidebarUpdate: true, }) @@ -93,6 +95,8 @@ const Create = ({ mutate }) => { defaultValues={{ explanation: "", category_id: "", + item_id: null, + sub_item_id: null, road_observed_id: "", rahdaran: [], requested_machines: [], diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/DataBox.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/DataBox.jsx index 5768f75..3253145 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/DataBox.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/DataBox.jsx @@ -1,50 +1,135 @@ -import { Box, Button, TextField } from "@mui/material"; +import { Box, Stack, IconButton, Typography, Divider } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; -const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { +const ROW_COLORS = { + source: "#0EA37A", // سبز + waypoint: "#F5A623", // زرد/نارنجی + dest: "#E53935", // قرمز +}; + +const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null); + +const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => ( + + + + + {label} + + + {value || placeholder} + + + {onEdit && ( + + + + )} + {onRemove && ( + + + + )} + +); + +const DataBox = ({ + isSource, + isDest, + sourcePosition, + destPosition, + editingTarget, + onEditSource, + onEditDest, + waypoints = [], + onEditWaypoint, + onAddWaypoint, + onRemoveWaypoint, + canAddWaypoint, +}) => { if (!isSource) return null; return ( -
- + + {waypoints.map((wp, index) => ( + onEditWaypoint(index) : null} + onRemove={() => onRemoveWaypoint(index)} /> - {isSource && isDest && ( - - )} -
+ ))} + {isDest && ( -
- - -
+ + )} + + {canAddWaypoint && ( + <> + + + + + افزودن نقطه میانی + + + )}
); diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/ShowRoute.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/ShowRoute.jsx index 6827cde..b9d5143 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/ShowRoute.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/ShowRoute.jsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { Marker, Polyline, useMap } from "react-leaflet"; import L from "leaflet"; import polyline from "@mapbox/polyline"; @@ -20,12 +20,22 @@ const ShowRoute = ({ area }) => { const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); - useEffect(() => { - if (coords.length > 0) { - map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 }); - } - }, [area]); + const [showRoute, setShowRoute] = useState(false); + useEffect(() => { + if (!coords.length) return; + + setShowRoute(false); + + map.flyToBounds(coords, { + padding: [50, 50], + duration: 1.5, + }); + + map.once("moveend", () => { + setShowRoute(true); + }); + }, [area]); if (!area) return null; const coords = polyline.decode(area, 6); @@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => { return ( <> - + {showRoute && } + @@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => { }; export default ShowRoute; + +// import { useEffect, useState } 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"; +// import WaypointIcon from "@/assets/images/waypoint-icon.svg"; +// +// 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]], +// }); +// }; +// +// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); +// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); +// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src); +// +// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده +// const ShowRoute = ({ area, locations = [] }) => { +// const map = useMap(); +// const [showRoute, setShowRoute] = useState(false); +// +// const coords = area ? polyline.decode(area, 6) : []; +// +// useEffect(() => { +// if (!coords.length) return; +// +// setShowRoute(false); +// +// map.flyToBounds(coords, { +// padding: [50, 50], +// duration: 1.5, +// }); +// +// map.once("moveend", () => { +// setShowRoute(true); +// }); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [area]); +// +// if (!area || coords.length === 0) return null; +// +// const sourcePosition = coords[0]; +// const destPosition = coords[coords.length - 1]; +// +// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری) +// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : []; +// +// return ( +// <> +// {showRoute && } +// +// +// +// {middleWaypoints.map((wp, index) => ( +// +// ))} +// +// +// +// ); +// }; +// +// export default ShowRoute; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/index.jsx index 49c4651..6d7fc4e 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/Area/Routing/index.jsx @@ -5,19 +5,29 @@ 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 WaypointIcon from "@/assets/images/prev-waypoint-icon.svg"; +import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg"; import { useEffect, useRef, useState } from "react"; import DataBox from "./DataBox"; import AlarmText from "./AlarmText"; +const MAX_WAYPOINTS = 2; + const Routing = ({ setArea, setLocations, locations, area }) => { const mapPrevSourceMarker = useRef(); const mapPrevDestMarker = useRef(); + const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint 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 [waypoints, setWaypoints] = useState([]); + const [routeCoords, setRouteCoords] = useState([]); const [alternateCoords, setAlternateCoords] = useState([]); const [editingTarget, setEditingTarget] = useState(null); @@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => { isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src ); - const isMovingSource = !isSource || editingTarget === "source"; - const isMovingDest = isSource && (!isDest || editingTarget === "dest"); + const getWaypointIcon = (index) => { + const wp = waypoints[index]; + const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`; + return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src); + }; + + const getActiveTarget = () => { + if (editingTarget) return editingTarget; + if (!isSource) return "source"; + const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed); + if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`; + if (!isDest) return "dest"; + return null; + }; const map = useMapEvents({ move(e) { - if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); - if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); + const target = getActiveTarget(); + const center = e.target.getCenter(); + if (target === "source") { + mapPrevSourceMarker.current?.setLatLng(center); + } else if (target === "dest") { + mapPrevDestMarker.current?.setLatLng(center); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setLatLng(center); + } }, movestart() { - if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src)); + } }, moveend() { - if (isMovingSource) - mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src)); + } }, }); @@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => { 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); + const first = { lat: locations[0].lat, lng: locations[0].lon }; + const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon }; + const middle = locations.slice(1, -1).map((loc) => ({ + position: { lat: loc.lat, lng: loc.lon }, + confirmed: true, + })); + + setSourcePosition(first); + setDestPosition(last); + setWaypoints(middle); setIsSource(true); setIsDest(true); + if (area) { const decoded = polyline.decode(area, 6); setRouteCoords(decoded); @@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => { mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); }, 100); } else { - fetchRoute(restoredSource, restoredDest); + fetchRoute(first, last, middle); } }, []); - const fetchRoute = async (source, dest) => { + const fetchRoute = async (source, dest, waypointsList = waypoints) => { + const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position); const locs = [ { lat: source.lat, lon: source.lng }, + ...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })), { lat: dest.lat, lon: dest.lng }, ]; try { @@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => { } }; + const resetRoute = () => { + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + }; + const handleMarkerClick = async () => { if (!isSource || editingTarget === "source") { const clickedPosition = mapPrevSourceMarker.current.getLatLng(); setSourcePosition(clickedPosition); setIsSource(true); + const wasEditing = editingTarget === "source"; setEditingTarget(null); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); - if (editingTarget === "source" && destPosition) { - await fetchRoute(clickedPosition, destPosition); + if (wasEditing && destPosition) { + await fetchRoute(clickedPosition, destPosition, waypoints); } } }; @@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => { setDestPosition(clickedPosition); setIsDest(true); setEditingTarget(null); + resetRoute(); - await fetchRoute(sourcePosition, clickedPosition); + await fetchRoute(sourcePosition, clickedPosition, waypoints); + } + }; + + const handleWaypointMarkerClick = async (index) => { + const wp = waypoints[index]; + if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت) + + const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng(); + const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w)); + setWaypoints(updated); + const wasEditing = editingTarget === `waypoint-${index}`; + setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); } }; const handleEditSource = () => { setEditingTarget("source"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); }; const handleEditDest = () => { setEditingTarget("dest"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); + }; + + const handleEditWaypoint = (index) => { + // برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه + setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w))); + setEditingTarget(`waypoint-${index}`); + resetRoute(); + }; + + // اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS) + const handleAddWaypoint = () => { + if (waypoints.length >= MAX_WAYPOINTS) return; + if (!isSource) return; // قبل از انتخاب مبدا معنی نداره + setWaypoints((prev) => [...prev, { position: null, confirmed: false }]); + setEditingTarget(`waypoint-${waypoints.length}`); + resetRoute(); + }; + + // حذف یک نقطه میانی + const handleRemoveWaypoint = async (index) => { + const updated = waypoints.filter((_, i) => i !== index); + setWaypoints(updated); + mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index); + if (editingTarget === `waypoint-${index}`) setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); + } }; const handleSelectRoute = (index, coords) => { @@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => { editingTarget={editingTarget} onEditSource={handleEditSource} onEditDest={handleEditDest} + waypoints={waypoints} + onEditWaypoint={handleEditWaypoint} + onAddWaypoint={handleAddWaypoint} + onRemoveWaypoint={handleRemoveWaypoint} + canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS} /> + + + {waypoints.map((wp, index) => ( + (mapPrevWaypointMarkers.current[index] = el)} + eventHandlers={{ click: () => handleWaypointMarkerClick(index) }} + icon={getWaypointIcon(index)} + /> + ))} + {isSource && ( { icon={prevDestIcon} /> )} + {alternateCoords.map((coords, index) => ( { + const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems(); + function getItemName(val) { + return itemsList.find((item) => item.id === val)?.name; + } + + return ( + ( + { + field.onChange(value); + setValue("explanation", getItemName(value)); + setValue("sub_item_id", ""); + }} + /> + )} + /> + ); +}; +export default ItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/SubItemSelectField.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/SubItemSelectField.jsx new file mode 100644 index 0000000..9d54a52 --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/SubItemSelectField.jsx @@ -0,0 +1,37 @@ +import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems"; +import { Controller } from "react-hook-form"; +import SelectBox from "@/core/components/SelectBox"; + +const SubItemSelectField = ({ control, watch, setValue, getValues }) => { + const itemId = watch("item_id"); + const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId); + + function createExplanation(val) { + const currentExplanation = getValues("explanation"); + const selectedItem = subItemsList.find((item) => item.id === val)?.name; + return `${currentExplanation}-${selectedItem}`; + } + + return ( + ( + { + field.onChange(val); + setValue("explanation", createExplanation(val)); + }} + /> + )} + /> + ); +}; +export default SubItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/index.jsx index 666b1f9..e4a0c57 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/GetItemInfo/index.jsx @@ -8,10 +8,12 @@ import { ExitToApp } from "@mui/icons-material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Controller, useForm } from "react-hook-form"; import { object, string } from "yup"; -import FastReactCode from "./FastReactCode"; +import ItemSelectField from "./ItemSelectField"; +import SubItemSelectField from "./SubItemSelectField"; const validationSchema = object({ - explanation: string().required("موضوع را مشخص کنید!"), + item_id: string().required("موضوع را مشخص کنید!"), + sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"), end_point: string().required("مقصد را مشخص کنید!"), region: string().required("محور ماموریت را مشخص کنید!"), category_id: string().required("نوع ماموریت را مشخص کنید!"), @@ -24,6 +26,8 @@ const validationSchema = object({ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const defaultValues = { + item_id: allData.item_id, + sub_item_id: allData.sub_item_id, explanation: allData.explanation, end_point: allData.end_point, region: allData.region, @@ -31,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { road_observed_id: allData.road_observed_id, }; - const { control, handleSubmit } = useForm({ + const { control, handleSubmit, watch, setValue, getValues } = useForm({ defaultValues, resolver: yupResolver(validationSchema), mode: "all", @@ -49,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { - + + + ( - - )} + watch={watch} + setValue={setValue} + getValues={getValues} /> diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/index.jsx index 2d5973e..32437c6 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/Form/index.jsx @@ -1,12 +1,11 @@ -import { AccessTime, Engineering, InsertDriveFile, Person, Verified, Map } from "@mui/icons-material"; +import { Engineering, InsertDriveFile, Map, Person, Verified } from "@mui/icons-material"; import { Box, Tab, Tabs } from "@mui/material"; import { useReducer, useState } from "react"; -import GetDateTime from "./GetDateTime"; +import Area from "./Area"; import GetItemInfo from "./GetItemInfo"; import MachineAndDriver from "./MachineAndDriver"; import Rahdaran from "./Rahdaran"; import Verify from "./Verify"; -import Area from "./Area"; function TabPanel(props) { const { children, value, index } = props; diff --git a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/index.jsx b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/index.jsx index 8c3ea66..0663c6a 100644 --- a/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/index.jsx +++ b/src/components/dashboard/roadMissions/operator/Actions/Violations/RowActions/MissionCorrection/index.jsx @@ -26,6 +26,8 @@ const MissionCorrection = ({ row, mutate, baseMutate }) => { end_point: result.end_point, driver: result.driver.id, encoded_route: result.area, + item_id: result.item_id, + sub_item_id: result.sub_item_id, }, hasSidebarUpdate: true, }) 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 index 5768f75..3253145 100644 --- 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 @@ -1,50 +1,135 @@ -import { Box, Button, TextField } from "@mui/material"; +import { Box, Stack, IconButton, Typography, Divider } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; -const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { +const ROW_COLORS = { + source: "#0EA37A", // سبز + waypoint: "#F5A623", // زرد/نارنجی + dest: "#E53935", // قرمز +}; + +const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null); + +const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => ( + + + + + {label} + + + {value || placeholder} + + + {onEdit && ( + + + + )} + {onRemove && ( + + + + )} + +); + +const DataBox = ({ + isSource, + isDest, + sourcePosition, + destPosition, + editingTarget, + onEditSource, + onEditDest, + waypoints = [], + onEditWaypoint, + onAddWaypoint, + onRemoveWaypoint, + canAddWaypoint, +}) => { if (!isSource) return null; return ( -
- + + {waypoints.map((wp, index) => ( + onEditWaypoint(index) : null} + onRemove={() => onRemoveWaypoint(index)} /> - {isSource && isDest && ( - - )} -
+ ))} + {isDest && ( -
- - -
+ + )} + + {canAddWaypoint && ( + <> + + + + + افزودن نقطه میانی + + + )}
); 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 index 7d47ba8..b9d5143 100644 --- 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 @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { Marker, Polyline, useMap } from "react-leaflet"; import L from "leaflet"; import polyline from "@mapbox/polyline"; @@ -18,14 +18,24 @@ const ShowRoute = ({ area }) => { }); }; - 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); + const [showRoute, setShowRoute] = useState(false); + useEffect(() => { + if (!coords.length) return; + + setShowRoute(false); + + map.flyToBounds(coords, { + padding: [50, 50], + duration: 1.5, + }); + + map.once("moveend", () => { + setShowRoute(true); + }); + }, [area]); if (!area) return null; const coords = polyline.decode(area, 6); @@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => { return ( <> - + {showRoute && } + @@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => { }; export default ShowRoute; + +// import { useEffect, useState } 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"; +// import WaypointIcon from "@/assets/images/waypoint-icon.svg"; +// +// 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]], +// }); +// }; +// +// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); +// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); +// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src); +// +// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده +// const ShowRoute = ({ area, locations = [] }) => { +// const map = useMap(); +// const [showRoute, setShowRoute] = useState(false); +// +// const coords = area ? polyline.decode(area, 6) : []; +// +// useEffect(() => { +// if (!coords.length) return; +// +// setShowRoute(false); +// +// map.flyToBounds(coords, { +// padding: [50, 50], +// duration: 1.5, +// }); +// +// map.once("moveend", () => { +// setShowRoute(true); +// }); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [area]); +// +// if (!area || coords.length === 0) return null; +// +// const sourcePosition = coords[0]; +// const destPosition = coords[coords.length - 1]; +// +// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری) +// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : []; +// +// return ( +// <> +// {showRoute && } +// +// +// +// {middleWaypoints.map((wp, index) => ( +// +// ))} +// +// +// +// ); +// }; +// +// 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 index 49c4651..6d7fc4e 100644 --- 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 @@ -5,19 +5,29 @@ 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 WaypointIcon from "@/assets/images/prev-waypoint-icon.svg"; +import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg"; import { useEffect, useRef, useState } from "react"; import DataBox from "./DataBox"; import AlarmText from "./AlarmText"; +const MAX_WAYPOINTS = 2; + const Routing = ({ setArea, setLocations, locations, area }) => { const mapPrevSourceMarker = useRef(); const mapPrevDestMarker = useRef(); + const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint 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 [waypoints, setWaypoints] = useState([]); + const [routeCoords, setRouteCoords] = useState([]); const [alternateCoords, setAlternateCoords] = useState([]); const [editingTarget, setEditingTarget] = useState(null); @@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => { isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src ); - const isMovingSource = !isSource || editingTarget === "source"; - const isMovingDest = isSource && (!isDest || editingTarget === "dest"); + const getWaypointIcon = (index) => { + const wp = waypoints[index]; + const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`; + return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src); + }; + + const getActiveTarget = () => { + if (editingTarget) return editingTarget; + if (!isSource) return "source"; + const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed); + if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`; + if (!isDest) return "dest"; + return null; + }; const map = useMapEvents({ move(e) { - if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); - if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); + const target = getActiveTarget(); + const center = e.target.getCenter(); + if (target === "source") { + mapPrevSourceMarker.current?.setLatLng(center); + } else if (target === "dest") { + mapPrevDestMarker.current?.setLatLng(center); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setLatLng(center); + } }, movestart() { - if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src)); + } }, moveend() { - if (isMovingSource) - mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); - if (isMovingDest && mapPrevDestMarker.current) - mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + const target = getActiveTarget(); + if (target === "source") { + mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); + } else if (target === "dest") { + mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); + } else if (target?.startsWith("waypoint-")) { + const idx = Number(target.split("-")[1]); + mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src)); + } }, }); @@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => { 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); + const first = { lat: locations[0].lat, lng: locations[0].lon }; + const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon }; + const middle = locations.slice(1, -1).map((loc) => ({ + position: { lat: loc.lat, lng: loc.lon }, + confirmed: true, + })); + + setSourcePosition(first); + setDestPosition(last); + setWaypoints(middle); setIsSource(true); setIsDest(true); + if (area) { const decoded = polyline.decode(area, 6); setRouteCoords(decoded); @@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => { mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); }, 100); } else { - fetchRoute(restoredSource, restoredDest); + fetchRoute(first, last, middle); } }, []); - const fetchRoute = async (source, dest) => { + const fetchRoute = async (source, dest, waypointsList = waypoints) => { + const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position); const locs = [ { lat: source.lat, lon: source.lng }, + ...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })), { lat: dest.lat, lon: dest.lng }, ]; try { @@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => { } }; + const resetRoute = () => { + setRouteCoords([]); + setAlternateCoords([]); + setSelectedRoute(null); + }; + const handleMarkerClick = async () => { if (!isSource || editingTarget === "source") { const clickedPosition = mapPrevSourceMarker.current.getLatLng(); setSourcePosition(clickedPosition); setIsSource(true); + const wasEditing = editingTarget === "source"; setEditingTarget(null); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); - if (editingTarget === "source" && destPosition) { - await fetchRoute(clickedPosition, destPosition); + if (wasEditing && destPosition) { + await fetchRoute(clickedPosition, destPosition, waypoints); } } }; @@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => { setDestPosition(clickedPosition); setIsDest(true); setEditingTarget(null); + resetRoute(); - await fetchRoute(sourcePosition, clickedPosition); + await fetchRoute(sourcePosition, clickedPosition, waypoints); + } + }; + + const handleWaypointMarkerClick = async (index) => { + const wp = waypoints[index]; + if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت) + + const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng(); + const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w)); + setWaypoints(updated); + const wasEditing = editingTarget === `waypoint-${index}`; + setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); } }; const handleEditSource = () => { setEditingTarget("source"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); }; const handleEditDest = () => { setEditingTarget("dest"); - setRouteCoords([]); - setAlternateCoords([]); - setSelectedRoute(null); + resetRoute(); + }; + + const handleEditWaypoint = (index) => { + // برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه + setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w))); + setEditingTarget(`waypoint-${index}`); + resetRoute(); + }; + + // اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS) + const handleAddWaypoint = () => { + if (waypoints.length >= MAX_WAYPOINTS) return; + if (!isSource) return; // قبل از انتخاب مبدا معنی نداره + setWaypoints((prev) => [...prev, { position: null, confirmed: false }]); + setEditingTarget(`waypoint-${waypoints.length}`); + resetRoute(); + }; + + // حذف یک نقطه میانی + const handleRemoveWaypoint = async (index) => { + const updated = waypoints.filter((_, i) => i !== index); + setWaypoints(updated); + mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index); + if (editingTarget === `waypoint-${index}`) setEditingTarget(null); + resetRoute(); + + if (sourcePosition && destPosition) { + await fetchRoute(sourcePosition, destPosition, updated); + } }; const handleSelectRoute = (index, coords) => { @@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => { editingTarget={editingTarget} onEditSource={handleEditSource} onEditDest={handleEditDest} + waypoints={waypoints} + onEditWaypoint={handleEditWaypoint} + onAddWaypoint={handleAddWaypoint} + onRemoveWaypoint={handleRemoveWaypoint} + canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS} /> + + + {waypoints.map((wp, index) => ( + (mapPrevWaypointMarkers.current[index] = el)} + eventHandlers={{ click: () => handleWaypointMarkerClick(index) }} + icon={getWaypointIcon(index)} + /> + ))} + {isSource && ( { icon={prevDestIcon} /> )} + {alternateCoords.map((coords, index) => ( { + const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems(); + function getItemName(val) { + return itemsList.find((item) => item.id === val)?.name; + } + + return ( + ( + { + field.onChange(value); + setValue("explanation", getItemName(value)); + setValue("sub_item_id", ""); + }} + /> + )} + /> + ); +}; +export default ItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/SubItemSelectField.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/SubItemSelectField.jsx new file mode 100644 index 0000000..9d54a52 --- /dev/null +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/SubItemSelectField.jsx @@ -0,0 +1,37 @@ +import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems"; +import { Controller } from "react-hook-form"; +import SelectBox from "@/core/components/SelectBox"; + +const SubItemSelectField = ({ control, watch, setValue, getValues }) => { + const itemId = watch("item_id"); + const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId); + + function createExplanation(val) { + const currentExplanation = getValues("explanation"); + const selectedItem = subItemsList.find((item) => item.id === val)?.name; + return `${currentExplanation}-${selectedItem}`; + } + + return ( + ( + { + field.onChange(val); + setValue("explanation", createExplanation(val)); + }} + /> + )} + /> + ); +}; +export default SubItemSelectField; diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/index.jsx index ce38e38..0edafdf 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Add/AddController/Dialog/Form/GetItemInfo/index.jsx @@ -1,3 +1,4 @@ +import LtrTextField from "@/core/components/LtrTextField"; import PersianTextField from "@/core/components/PersianTextField"; import SelectBox from "@/core/components/SelectBox"; import StyledForm from "@/core/components/StyledForm"; @@ -8,7 +9,8 @@ import { ExitToApp } from "@mui/icons-material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Controller, useForm } from "react-hook-form"; import { object, string } from "yup"; -import LtrTextField from "@/core/components/LtrTextField"; +import ItemSelectField from "./ItemSelectField"; +import SubItemSelectField from "./SubItemSelectField"; const validationSchema = object({ explanation: string().required("موضوع را مشخص کنید!"), @@ -24,6 +26,8 @@ const validationSchema = object({ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const defaultValues = { + item_id: allData.item_id, + sub_item_id: allData.sub_item_id, explanation: allData.explanation, end_point: allData.end_point, region: allData.region, @@ -31,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { road_observed_id: allData.road_observed_id, }; - const { control, handleSubmit } = useForm({ + const { control, handleSubmit, watch, setValue, getValues } = useForm({ defaultValues, resolver: yupResolver(validationSchema), mode: "all", @@ -49,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { - + + + ( - - )} + watch={watch} + setValue={setValue} + getValues={getValues} /> @@ -126,7 +120,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { }} /> - + { خودرو - + {allData.machine && ( + + )} {/* راننده 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 0c7f4d9..c94039d 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 @@ -9,6 +9,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { const requestServer = useRequest({ notificationSuccess: true }); const submitForm = async (result) => { + console.log("result: ", result); + setSubmitting(true); await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", { data: { @@ -29,6 +31,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { end_point: result.end_point, category_id: result.category_id, explanation: result.explanation, + item_id: result.item_id, + sub_item_id: result.sub_item_id, ...(result.category_id == 3 ? { road_observed_id: result.road_observed_id, @@ -51,6 +55,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => { !r.is_driver), bound_type: "polyline", diff --git a/src/components/dashboard/roadMissions/operator/RowActions/Edit/EditController/index.jsx b/src/components/dashboard/roadMissions/operator/RowActions/Edit/EditController/index.jsx index e838647..93b791c 100644 --- a/src/components/dashboard/roadMissions/operator/RowActions/Edit/EditController/index.jsx +++ b/src/components/dashboard/roadMissions/operator/RowActions/Edit/EditController/index.jsx @@ -51,6 +51,8 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => { await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", { data: { explanation: result.explanation, + item_id: result.item_id, + sub_item_id: result.sub_item_id, category_id: result.category_id, ...(result.category_id == 3 @@ -101,6 +103,8 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {