9 Commits

20 changed files with 446 additions and 562 deletions

View File

@@ -1,5 +1,5 @@
HOST="rms.witel.ir"
HOST_RMTO="rms.rmto.ir"
NEXT_PUBLIC_VERSION="1.8.7"
NEXT_PUBLIC_VERSION="1.8.8"
NEXT_PUBLIC_API_URL="https://rms.witel.ir"
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"

View File

@@ -14,6 +14,7 @@ import useCities from "@/lib/hooks/useCities";
const ControlList = () => {
const columns = useMemo(() => {
const dynamicColumns = {
accessorKey: "province_id",
header: "استان",
id: "province_id",
enableColumnFilter: true,

View File

@@ -1,3 +1,4 @@
import LtrTextField from "@/core/components/LtrTextField";
import MuiDatePicker from "@/core/components/MuiDatePicker";
import MuiTimePicker from "@/core/components/MuiTimePicker";
import { FINISH_MISSION } from "@/core/utils/routes";
@@ -14,6 +15,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
const requestServer = useRequest({ notificationSuccess: true });
const validationSchema = object().shape({
end_km: string().required("لطفاً کیلومتر را وارد کنید."),
start_date: mixed().required("تاریخ خروج خودرو را مشخص کنید."),
start_time: mixed().required("ساعت خروج خودرو را مشخص کنید."),
});
@@ -24,6 +26,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
formState: { isSubmitting },
} = useForm({
defaultValues: {
end_km: "",
start_date: null,
start_time: null,
},
@@ -36,6 +39,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
const formattedTime = format(values.start_time, "HH:mm");
requestServer(`${FINISH_MISSION}/${rowId}`, "post", {
data: {
end_km: values.end_km,
time: `${formattedDate} ${formattedTime}`,
},
hasSidebarUpdate: true,
@@ -55,6 +59,27 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
<DialogContent>
<Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از ورود خودرو و پایان ماموریت اطمینان دارید؟</Typography>
<Stack>
<Controller
control={control}
name={"end_km"}
render={({ field, fieldState: { error } }) => (
<LtrTextField
autoComplete="off"
type="tel"
value={field.value}
onChange={(e) => {
if (isNaN(Number(e.target.value))) return;
field.onChange(e.target.value);
}}
size="small"
fullWidth
label="کیلومتر خودرو / ساعت کار"
InputLabelProps={{ shrink: true }}
/>
)}
/>
</Stack>
<Stack>
<Controller
control={control}
@@ -65,8 +90,8 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
error={error}
value={field.value || null}
disableFuture={false}
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
label={"تاریخ شروع ماموریت"}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
@@ -83,8 +108,8 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
error={error}
value={field.value || null}
views={["hours", "minutes"]}
placeholder={"زمان شروع ماموریت را وارد کنید"}
label={"زمان شروع ماموریت"}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>

View File

@@ -51,7 +51,6 @@ const MissionDates = ({ control }) => {
name="start_time"
error={error}
value={field.value}
views={["hours"]}
minTime={
start_date && new Date(start_date).toDateString() === now.toDateString()
? now
@@ -100,7 +99,6 @@ const MissionDates = ({ control }) => {
name="end_time"
error={error}
value={field.value}
views={["hours"]}
minTime={startDateTime ?? null}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}

View File

@@ -28,7 +28,6 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Chip color="primary" variant="outlined" label="مسیر ماموریت" />
</Divider>
<Box sx={{ width: "100%", height: "400px" }}>
{console.log("allData.area", allData.area)}
<MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={allData.area} />
</MapLayer>

View File

@@ -15,7 +15,7 @@ const ToDateController = ({ control }) => {
maxDate={new Date()}
error={!!error}
value={value}
label={"تاریخ شروع (تا تاریخ)"}
label={"تاریخ پایان (تا تاریخ)"}
placeholder={"تا تاریخ"}
setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");

View File

@@ -14,16 +14,10 @@ import MachinesDialog from "./MachinesDialog";
import Reject from "./Reject";
import SelectBox from "@/core/components/SelectBox";
import { Controller, useForm } from "react-hook-form";
import RouteDetail from "../../RouteDetail";
import ShowRoute from "../../RouteDetail/ShowRoute";
const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
const latLngs = useMemo(() => {
return row.area.coordinates.map(([lng, lat]) => [lat, lng]);
}, [row.area.coordinates]);
const bound = useMemo(
() => (row.area.type === "polygon" ? L.polygon(latLngs) : L.polyline(latLngs)),
[row.area.type, latLngs]
);
const requestServer = useRequest({ notificationSuccess: true });
const [machine, setMachine] = useState();
const [driver, setDriver] = useState();
@@ -180,10 +174,32 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
)}
</Stack>
</Stack>
<Box sx={{ width: "100%", height: "200px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
<Box
sx={{
width: "100%",
height: "200px",
justifyContent: "center",
alignItems: "center",
display: "flex",
}}
>
{row.encoded_route ? (
<MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={row.encoded_route} />
</MapLayer>
) : (
<Typography
sx={{
color: "#ff4a4c",
fontSize: "16px",
fontWeight: "bold",
textAlign: "center",
letterSpacing: "1px",
}}
>
مختصات برای این ماموریت در دسترس نمیباشد
</Typography>
)}
</Box>
</Stack>
</Box>

View File

@@ -1,129 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import L from "leaflet";
import { FeatureGroup, useMap } from "react-leaflet";
import "leaflet-draw";
const DrawBound = ({ control, controlDispach, bound, setBound }) => {
const map = useMap();
const featureRef = useRef(null);
const [area, setArea] = useState();
const drawControlRef = useRef(null);
const safeFlyToBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.flyToBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("flyToBounds failed:", e);
}
};
const safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("fitBounds failed:", e);
}
};
const bindEditEvent = (layer) => {
if (!layer) return;
layer.on("edit", function (e) {
const editedLayer = e.target;
setArea(editedLayer);
setBound(editedLayer);
safeFlyToBounds(editedLayer);
});
};
const handlerCreatedBound = useCallback((event) => {
const { layer } = event;
layer.editing.enable();
featureRef.current.addLayer(layer); // 🟢 اول اضافه به نقشه
setArea(layer);
setBound(layer);
safeFlyToBounds(layer);
bindEditEvent(layer);
controlDispach({ type: "SET_STATUS", status: 2 });
}, []);
useEffect(() => {
if (
control.status === 1 &&
drawControlRef.current &&
drawControlRef.current._toolbars?.draw?._modes?.polygon?.handler
) {
drawControlRef.current._toolbars.draw._modes.polygon.handler.enable();
}
}, [control.status]);
useEffect(() => {
if (control.status === 0 && area && featureRef.current) {
featureRef.current.removeLayer(area);
setArea(null);
setBound(null);
}
}, [control.status, area]);
useEffect(() => {
if (control.status === 2 && bound && featureRef.current) {
setArea(bound);
featureRef.current.addLayer(bound);
bound.editing.enable();
safeFitBounds(bound);
bindEditEvent(bound);
}
}, [control.status, bound]);
useEffect(() => {
if (!featureRef.current) return;
L.drawLocal.draw.handlers.polygon.tooltip.start = "برای شروع ترسیم محدوده، روی نقشه کلیک کنید";
L.drawLocal.draw.handlers.polygon.tooltip.cont = "برای ادامه ترسیم، نقاط بعدی را کلیک کنید";
L.drawLocal.draw.handlers.polygon.tooltip.end = "برای بستن محدوده، روی نقطه‌ی شروع کلیک کنید";
const drawControl = new L.Control.Draw({
draw: {
polygon: {
shapeOptions: {
color: "#3388ff",
weight: 3,
clickable: true,
},
},
polyline: false,
rectangle: false,
circle: false,
marker: false,
circlemarker: false,
},
edit: {
featureGroup: featureRef.current,
edit: true,
remove: false,
},
});
drawControlRef.current = drawControl;
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, handlerCreatedBound);
return () => {
map.off(L.Draw.Event.CREATED, handlerCreatedBound);
map.removeControl(drawControl);
};
}, [map, handlerCreatedBound]);
return <FeatureGroup ref={featureRef} />;
};
export default DrawBound;

View File

@@ -1,116 +0,0 @@
import { Delete, Route } from "@mui/icons-material";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useReducer } from "react";
import DrawBound from "./DrawBound";
const statusType = [
{
id: 0,
message: "برای آغاز ترسیم محدوده، کلیک کنید!",
buttons: [
{
label: "شروع ترسیم محدوده",
key: "start",
color: "primary",
icon: <Route />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 1 });
},
},
],
},
{
id: 1,
message: "محدوده‌ی موردنظر را با کلیک روی نقشه مشخص کنید. برای اتمام، روی نقطه‌ی ابتدایی کلیک کنید!",
buttons: [],
},
{
id: 2,
message: "برای اصلاح محدوده، گوشه‌ها را بکشید. برای ترسیم دوباره، آن را حذف کنید",
buttons: [
{
label: "حذف",
key: "end",
color: "error",
icon: <Delete />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 0 });
},
},
],
},
];
const createInitialState = (bound) => {
if (bound) {
return { status: 2 };
}
return { status: 0 };
};
const reducer = (state, action) => {
switch (action.type) {
case "SET_STATUS":
return { ...state, status: action.status };
default:
return state;
}
};
const MapControlPolygon = ({ bound, setBound }) => {
const [control, controlDispach] = useReducer(reducer, bound, createInitialState);
return (
<>
<DrawBound bound={bound} setBound={setBound} control={control} controlDispach={controlDispach} />
<Stack
direction={"row"}
justifyContent={"center"}
sx={{ position: "absolute", left: 0, top: 0, zIndex: 2000, width: "100%" }}
>
<Box
sx={{
background: (theme) => theme.palette.info.main,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
py: 0.5,
px: 2,
}}
>
<Typography align="center" component={"div"} variant="caption" color={"#fff"}>
{statusType.find((st) => st.id == control.status).message}
</Typography>
</Box>
</Stack>
<Box
sx={{
position: "absolute",
left: 0,
bottom: 0,
zIndex: 2000,
width: "100%",
py: 1,
background: "linear-gradient(to top, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0))",
}}
>
<Stack direction={"row"} justifyContent={"center"} spacing={2}>
{statusType
.find((st) => st.id == control.status)
.buttons.map((button) => (
<Button
size="small"
key={button.key}
variant="contained"
onClick={() => button.onclick(controlDispach)}
color={button.color}
startIcon={button.icon}
>
{button.label}
</Button>
))}
</Stack>
</Box>
</>
);
};
export default MapControlPolygon;

View File

@@ -1,129 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import L from "leaflet";
import { FeatureGroup, useMap } from "react-leaflet";
import "leaflet-draw";
const DrawBound = ({ control, controlDispach, bound, setBound }) => {
const map = useMap();
const featureRef = useRef(null);
const [area, setArea] = useState();
const drawControlRef = useRef(null);
const safeFlyToBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.flyToBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("flyToBounds failed:", e);
}
};
const safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("fitBounds failed:", e);
}
};
const bindEditEvent = (layer) => {
if (!layer) return;
layer.on("edit", function (e) {
const editedLayer = e.target;
setArea(editedLayer);
setBound(editedLayer);
safeFlyToBounds(editedLayer);
});
};
const handlerCreatedBound = useCallback((event) => {
const { layer } = event;
layer.editing.enable();
featureRef.current.addLayer(layer); // 🟢 اول اضافه به نقشه
setArea(layer);
setBound(layer);
safeFlyToBounds(layer);
bindEditEvent(layer);
controlDispach({ type: "SET_STATUS", status: 2 });
}, []);
useEffect(() => {
if (
control.status === 1 &&
drawControlRef.current &&
drawControlRef.current._toolbars?.draw?._modes?.polyline?.handler
) {
drawControlRef.current._toolbars.draw._modes.polyline.handler.enable();
}
}, [control.status]);
useEffect(() => {
if (control.status === 0 && area && featureRef.current) {
featureRef.current.removeLayer(area);
setArea(null);
setBound(null);
}
}, [control.status, area]);
useEffect(() => {
if (control.status === 2 && bound && featureRef.current) {
setArea(bound);
featureRef.current.addLayer(bound);
bound.editing.enable();
safeFitBounds(bound);
bindEditEvent(bound);
}
}, [control.status, bound]);
useEffect(() => {
if (!featureRef.current) return;
L.drawLocal.draw.handlers.polyline.tooltip.start = "برای شروع ترسیم مسیر، روی نقشه کلیک کنید";
L.drawLocal.draw.handlers.polyline.tooltip.cont = "برای ادامه ترسیم، نقاط بعدی را کلیک کنید";
L.drawLocal.draw.handlers.polyline.tooltip.end = "برای بستن مسیر، روی نقطه پایانی کلیک کنید";
const drawControl = new L.Control.Draw({
draw: {
polyline: {
shapeOptions: {
color: "#3388ff",
weight: 3,
clickable: true,
},
},
polygon: false,
rectangle: false,
circle: false,
marker: false,
circlemarker: false,
},
edit: {
featureGroup: featureRef.current,
edit: true,
remove: false,
},
});
drawControlRef.current = drawControl;
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, handlerCreatedBound);
return () => {
map.off(L.Draw.Event.CREATED, handlerCreatedBound);
map.removeControl(drawControl);
};
}, [map, handlerCreatedBound]);
return <FeatureGroup ref={featureRef} />;
};
export default DrawBound;

View File

@@ -1,116 +0,0 @@
import { Delete, Route } from "@mui/icons-material";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useReducer } from "react";
import DrawBound from "./DrawBound";
const statusType = [
{
id: 0,
message: "برای آغاز ترسیم مسیر، کلیک کنید!",
buttons: [
{
label: "شروع ترسیم مسیر",
key: "start",
color: "primary",
icon: <Route />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 1 });
},
},
],
},
{
id: 1,
message: "مسیر موردنظر را با کلیک روی نقشه مشخص کنید. برای اتمام، روی نقطه‌ی پایانی کلیک کنید!",
buttons: [],
},
{
id: 2,
message: "برای اصلاح مسیر، گوشه‌ها را بکشید. برای ترسیم دوباره، آن را حذف کنید",
buttons: [
{
label: "حذف",
key: "end",
color: "error",
icon: <Delete />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 0 });
},
},
],
},
];
const createInitialState = (bound) => {
if (bound) {
return { status: 2 };
}
return { status: 0 };
};
const reducer = (state, action) => {
switch (action.type) {
case "SET_STATUS":
return { ...state, status: action.status };
default:
return state;
}
};
const MapControlPolyline = ({ bound, setBound }) => {
const [control, controlDispach] = useReducer(reducer, bound, createInitialState);
return (
<>
<DrawBound bound={bound} setBound={setBound} control={control} controlDispach={controlDispach} />
<Stack
direction={"row"}
justifyContent={"center"}
sx={{ position: "absolute", left: 0, top: 0, zIndex: 2000, width: "100%" }}
>
<Box
sx={{
background: (theme) => theme.palette.info.main,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
py: 0.5,
px: 2,
}}
>
<Typography align="center" component={"div"} variant="caption" color={"#fff"}>
{statusType.find((st) => st.id == control.status).message}
</Typography>
</Box>
</Stack>
<Box
sx={{
position: "absolute",
left: 0,
bottom: 0,
zIndex: 2000,
width: "100%",
py: 1,
background: "linear-gradient(to top, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0))",
}}
>
<Stack direction={"row"} justifyContent={"center"} spacing={2}>
{statusType
.find((st) => st.id == control.status)
.buttons.map((button) => (
<Button
size="small"
key={button.key}
variant="contained"
onClick={() => button.onclick(controlDispach)}
color={button.color}
startIcon={button.icon}
>
{button.label}
</Button>
))}
</Stack>
</Box>
</>
);
};
export default MapControlPolyline;

View File

@@ -0,0 +1,28 @@
import { Box, Typography } from "@mui/material";
const AlarmText = ({ isSource }) => {
if (isSource) return null;
return (
<Box
sx={{
position: "absolute",
top: 5,
left: 5,
zIndex: 1000,
border: "1px solid #ed0f02",
backgroundColor: "#f19898",
py: 0.5,
px: 1,
borderRadius: 2,
opacity: 0.8,
boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px",
}}
>
<Typography variant="caption" sx={{ fontWeight: "bold" }} color="#ed0f02">
با کلیک بر روی مارکر موقعیت را انتخاب کنید
</Typography>
</Box>
);
};
export default AlarmText;

View File

@@ -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 (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
)}
</Box>
);
};
export default DataBox;

View File

@@ -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]],
});
};
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];
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
return (
<>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
<Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} />
</>
);
};
export default ShowRoute;

View File

@@ -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 (
<>
<DataBox
isSource={isSource}
isDest={isDest}
sourcePosition={sourcePosition}
destPosition={destPosition}
editingTarget={editingTarget}
onEditSource={handleEditSource}
onEditDest={handleEditDest}
/>
<AlarmText isSource={isSource} />
<Marker
position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon}
/>
{isSource && (
<Marker
position={destPosition || map.getCenter()}
ref={mapPrevDestMarker}
eventHandlers={{ click: handleDestMarkerClick }}
icon={prevDestIcon}
/>
)}
{alternateCoords.map((coords, index) => (
<Polyline
key={`alternate-${index}-${selectedRoute?.index === index}`}
positions={coords}
color={selectedRoute?.index === index ? "#015688" : "#91B2C6"}
weight={selectedRoute?.index === index ? 7 : 6}
opacity={selectedRoute?.index === index ? 1 : 0.7}
eventHandlers={{ click: () => handleSelectRoute(index, coords) }}
/>
))}
{routeCoords.length > 0 && (
<Polyline
key={`main-${selectedRoute?.index === "main"}`}
positions={routeCoords}
color={selectedRoute?.index === "main" ? "#015688" : "#91B2C6"}
weight={selectedRoute?.index === "main" ? 7 : 6}
opacity={selectedRoute?.index === "main" ? 1 : 0.7}
eventHandlers={{ click: () => handleSelectRoute("main", routeCoords) }}
/>
)}
</>
);
};
export default Routing;

View File

@@ -1,25 +0,0 @@
import SelectBox from "@/core/components/SelectBox";
export const boundTypes = [
{ id: "polygon", name_fa: "محدوده" },
{ id: "polyline", name_fa: "مسیر" },
];
const SelectBoundType = ({ boundType, setBoundType, setBound, setAllData }) => {
return (
<>
<SelectBox
value={boundType}
label="نوع منطقه عملیاتی"
selectors={boundTypes}
schema={{ name: "name_fa", value: "id" }}
onChange={(newValue) => {
setBound(null);
setBoundType(newValue);
setAllData({ bound_type: newValue });
}}
/>
</>
);
};
export default SelectBoundType;

View File

@@ -2,40 +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 Routing from "./Routing";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
const [bound, setBound] = useState(allData.bound);
const [boundType, setBoundType] = useState(allData.bound_type);
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 (
<>
<DialogContent dividers>
<Stack spacing={2}>
<SelectBoundType
boundType={boundType}
setBoundType={setBoundType}
setBound={setBound}
setAllData={setAllData}
/>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
{boundType == "polygon" ? (
<MapControlPolygon bound={bound} setBound={setBound} boundType={boundType} />
) : (
<MapControlPolyline bound={bound} setBound={setBound} boundType={boundType} />
)}
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
</MapLayer>
</Box>
</Stack>
@@ -44,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
</Button>
</DialogActions>
</>

View File

@@ -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 "../../../showBound";
import ShowRoute from "../Area/Routing/ShowRoute";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
@@ -22,6 +22,16 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Box sx={{ flex: 1 }}>
<Stack spacing={2}>
<Stack spacing={1}>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="مسیر ماموریت" />
</Divider>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={allData.area} />
</MapLayer>
</Box>
</Stack>
<Divider>
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />
</Divider>

View File

@@ -1,4 +1,4 @@
import { AccessTime, Engineering, InsertDriveFile, Person, Verified } from "@mui/icons-material";
import { AccessTime, Engineering, InsertDriveFile, Person, Verified, Map } from "@mui/icons-material";
import { Box, Tab, Tabs } from "@mui/material";
import { useReducer, useState } from "react";
import GetDateTime from "./GetDateTime";
@@ -6,6 +6,7 @@ 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;
@@ -61,8 +62,9 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting }) => {
<Tab icon={<InsertDriveFile />} label="مشخصات" />
<Tab disabled={tabState < 1} icon={<AccessTime />} label="زمانبندی" />
<Tab disabled={tabState < 2} icon={<Person />} label="راننده" />
<Tab disabled={tabState < 3} icon={<Engineering />} label="همراهان" />
<Tab disabled={tabState < 4} icon={<Verified />} label="بررسی نهایی" />
<Tab disabled={tabState < 3} icon={<Map />} label="مختصات" />
<Tab disabled={tabState < 4} icon={<Engineering />} label="همراهان" />
<Tab disabled={tabState < 5} icon={<Verified />} label="بررسی نهایی" />
</Tabs>
<TabPanel value={tabState} index={0}>
<GetItemInfo
@@ -95,7 +97,7 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting }) => {
/>
</TabPanel>
<TabPanel value={tabState} index={3}>
<Rahdaran
<Area
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
@@ -105,6 +107,16 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting }) => {
/>
</TabPanel>
<TabPanel value={tabState} index={4}>
<Rahdaran
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={5}>
<Verify allData={allData} handlePrev={handlePrev} submitForm={submitForm} submitting={submitting} />
</TabPanel>
</>

View File

@@ -28,6 +28,7 @@ const CreateWithoutProcess = ({ row, mutate }) => {
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
driver: result.driver.id,
encoded_route: result.area,
},
hasSidebarUpdate: true,
})
@@ -49,7 +50,7 @@ const CreateWithoutProcess = ({ row, mutate }) => {
</IconButton>
</Tooltip>
<Dialog open={open} fullWidth maxWidth="sm">
<Dialog open={open} fullWidth maxWidth="md">
<IconButton
aria-label="close"
onClick={() => setOpen(false)}
@@ -77,6 +78,8 @@ const CreateWithoutProcess = ({ row, mutate }) => {
end_point: "",
region: "",
driver: null,
area: null,
locations: [],
}}
submitForm={submitForm}
submitting={submitting}