diff --git a/.env.example b/.env.example
index 8368bd5..d6c1f1b 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,5 @@
HOST="rms.witel.ir"
+HOST_RMTO="rms.rmto.ir"
NEXT_PUBLIC_VERSION="1.8.3"
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/next.config.mjs b/next.config.mjs
index e33199c..ac3621e 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -7,6 +7,10 @@ const nextConfig = {
protocol: 'https',
hostname: process.env.HOST,
},
+ {
+ protocol: 'https',
+ hostname: process.env.HOST_RMTO,
+ },
],
},
};
diff --git a/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/loading.jsx b/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/loading.jsx
new file mode 100644
index 0000000..4535a8d
--- /dev/null
+++ b/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/loading.jsx
@@ -0,0 +1,7 @@
+"use client";
+import PageLoading from "@/core/components/PageLoading";
+
+const Loading = () => {
+ return ;
+};
+export default Loading;
diff --git a/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/page.js b/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/page.js
new file mode 100644
index 0000000..a4cad6b
--- /dev/null
+++ b/src/app/(withAuth)/(dashboardLayout)/infrastructure/map/page.js
@@ -0,0 +1,16 @@
+import WithPermission from "@/core/middlewares/withPermission";
+import TollHouseMapPage from "@/components/infrastructure/tollHouseMap";
+
+export const metadata = {
+ title: "پراکندگی بر روی نقشه راهدارخانه ها",
+};
+
+const Page = () => {
+ return (
+
+
+
+ );
+};
+
+export default Page;
diff --git a/src/components/dashboard/roadMissions/control/RowActions/StartMission/StartMissionContent.jsx b/src/components/dashboard/roadMissions/control/RowActions/StartMission/StartMissionContent.jsx
index c7f516d..c935d94 100644
--- a/src/components/dashboard/roadMissions/control/RowActions/StartMission/StartMissionContent.jsx
+++ b/src/components/dashboard/roadMissions/control/RowActions/StartMission/StartMissionContent.jsx
@@ -11,6 +11,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
const validationSchema = object().shape({
code: string().length(6, "کد خروج باید 6 رقم باشد").required("لطفاً کد خروج را وارد کنید."),
+ km: string().required("لطفاً کیلومتر را وارد کنید."),
});
const {
@@ -20,6 +21,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
} = useForm({
defaultValues: {
code: "",
+ km: "",
},
resolver: yupResolver(validationSchema),
mode: "all",
@@ -29,6 +31,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
requestServer(`${START_MISSION}/${rowId}`, "post", {
data: {
code: values.code,
+ km: values.km,
},
hasSidebarUpdate: true,
})
@@ -69,6 +72,27 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
)}
/>
+
+ (
+ {
+ if (isNaN(Number(e.target.value))) return;
+ field.onChange(e.target.value);
+ }}
+ size="small"
+ fullWidth
+ label="کیلومتر خودرو / ساعت کار"
+ InputLabelProps={{ shrink: true }}
+ />
+ )}
+ />
+
diff --git a/src/components/dashboard/roadMissions/transportation/RowActions/Allocation/Form/index.jsx b/src/components/dashboard/roadMissions/transportation/RowActions/Allocation/Form/index.jsx
index f5d4395..70d8cfb 100644
--- a/src/components/dashboard/roadMissions/transportation/RowActions/Allocation/Form/index.jsx
+++ b/src/components/dashboard/roadMissions/transportation/RowActions/Allocation/Form/index.jsx
@@ -22,16 +22,14 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
const requestServer = useRequest({ notificationSuccess: true });
const [machine, setMachine] = useState();
const [driver, setDriver] = useState();
- const [kilometer, setKilometer] = useState();
const [isSubmitting, setIsSubmitting] = useState(false);
- const handleSubmit = async (_machine, _driver, _kilometer) => {
+ const handleSubmit = async (_machine, _driver) => {
setIsSubmitting(true);
await requestServer(`${ALLOCATE_REQUEST_MISSION}/${row.id}`, "post", {
data: {
- machines: [_machine.id],
+ machines: _machine.id,
driver: _driver.id,
- km: kilometer,
},
hasSidebarUpdate: true,
})
@@ -110,23 +108,6 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
)}
)}
- {driver && (
-
- {
- if (isNaN(Number(e.target.value))) return;
- setKilometer(e.target.value);
- }}
- size="small"
- fullWidth
- label="کیلومتر خودرو"
- InputLabelProps={{ shrink: true }}
- />
-
- )}
@@ -208,10 +189,10 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
+
+
+ {open && (
+
+ )}
+
+
+ >
+ );
+};
+export default Filter;
diff --git a/src/components/infrastructure/tollHouseMap/Legend/index.jsx b/src/components/infrastructure/tollHouseMap/Legend/index.jsx
new file mode 100644
index 0000000..b6f5936
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/Legend/index.jsx
@@ -0,0 +1,35 @@
+import { Box, Stack, Typography } from "@mui/material";
+
+const Legend = () => {
+ return (
+
+
+ theme.palette.success.main,
+ background: (theme) => theme.palette.success.light,
+ }}
+ />
+ فعال
+
+
+ theme.palette.error.main,
+ background: (theme) => theme.palette.error.light,
+ }}
+ />
+ غیرفعال
+
+
+ );
+};
+export default Legend;
diff --git a/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx b/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx
new file mode 100644
index 0000000..5061fe9
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx
@@ -0,0 +1,71 @@
+import { Box, Chip, Divider, Stack, Typography } from "@mui/material";
+import moment from "jalali-moment";
+import Image from "next/image";
+
+const ContentInfoItem = ({ data }) => {
+ return (
+
+
+
+
+ {data.id !== "" && data.id}
+
+
+
+
+
+ {data.province_name !== "" && data.province_name}
+
+
+
+
+
+
+ {data.type_fa !== "" && data.type_fa}
+
+
+
+
+
+
+ {data.status === "1" ? "فعال" : "غیرفعال"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+export default ContentInfoItem;
diff --git a/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/index.jsx b/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/index.jsx
new file mode 100644
index 0000000..232c2a4
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/PointsOnMap/DialogInfoItem/index.jsx
@@ -0,0 +1,51 @@
+import DialogLoading from "@/core/components/DialogLoading";
+import { GET_TOLL_HOUSE_DETAILS } from "@/core/utils/routes";
+import useRequest from "@/lib/hooks/useRequest";
+import { Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
+import { useEffect, useState } from "react";
+import ContentInfoItem from "./ContentInfoItem";
+
+const DialogInfoItem = ({ selectedId, closeDialog }) => {
+ const request = useRequest();
+ const [loading, setLoading] = useState(true);
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ const fetchItem = async () => {
+ setLoading(true);
+ setData(null);
+ try {
+ const response = await request(`${GET_TOLL_HOUSE_DETAILS}/${selectedId}`, "get", {
+ signal: controller.signal,
+ });
+ setData(response.data.data);
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchItem();
+
+ return () => {
+ controller.abort();
+ };
+ }, [selectedId]);
+
+ return (
+ <>
+ جزئیات فعالیت
+
+ {loading ? : data && }
+
+
+
+ بستن
+
+
+ >
+ );
+};
+export default DialogInfoItem;
diff --git a/src/components/infrastructure/tollHouseMap/PointsOnMap/index.jsx b/src/components/infrastructure/tollHouseMap/PointsOnMap/index.jsx
new file mode 100644
index 0000000..05b4d03
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/PointsOnMap/index.jsx
@@ -0,0 +1,77 @@
+import { Dialog, useTheme } from "@mui/material";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { CircleMarker, useMap } from "react-leaflet";
+import MarkerClusterGroup from "react-leaflet-markercluster";
+import DialogInfoItem from "./DialogInfoItem";
+
+const PointsOnMap = ({ data, isCluster }) => {
+ const map = useMap();
+ const theme = useTheme();
+ const [selectedId, setSelectedId] = useState(null);
+ const [open, setOpen] = useState(false);
+
+ const openDialog = useCallback(() => setOpen(true), []);
+ const closeDialog = useCallback(() => setOpen(false), []);
+
+ const getMarkerColor = (status) => {
+ if (status === 1) return theme.palette.warning.main;
+ if (status === 2) return theme.palette.error.main;
+ return theme.palette.success.main;
+ };
+
+ const markers = useMemo(() => {
+ return data.map(({ id, lat, lng, step }) => (
+ {
+ openDialog();
+ setSelectedId(id);
+ },
+ }}
+ color={getMarkerColor(Number(status))}
+ />
+ ));
+ }, [data, theme]);
+
+ useEffect(() => {
+ if (data.length === 0) return;
+
+ const bounds = L.latLngBounds(
+ data.map(({ lat, lng }) => {
+ return L.latLng(Number(lat), Number(lng));
+ })
+ );
+
+ map.flyToBounds(bounds, { duration: 0.7 });
+ }, [data]);
+
+ const createClusterCustomIcon = (cluster) => {
+ const count = cluster.getChildCount();
+ return L.divIcon({
+ html: `${count}
`,
+ className: "custom-marker-cluster",
+ iconSize: L.point(40, 40, true),
+ });
+ };
+
+ if (data.length === 0) return null;
+
+ return (
+ <>
+ {isCluster ? (
+
+ {markers}
+
+ ) : (
+ markers
+ )}
+
+ >
+ );
+};
+export default PointsOnMap;
diff --git a/src/components/infrastructure/tollHouseMap/index.jsx b/src/components/infrastructure/tollHouseMap/index.jsx
new file mode 100644
index 0000000..f7b5a86
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/index.jsx
@@ -0,0 +1,231 @@
+"use client";
+import LoadingHardPage from "@/core/components/LoadingHardPage";
+import MapLoading from "@/core/components/MapLayer/Loading";
+import isArrayEmpty from "@/core/utils/isArrayEmpty";
+import { GET_TOLL_HOUSE_MAP } from "@/core/utils/routes";
+import useRequest from "@/lib/hooks/useRequest";
+import { Box, Stack } from "@mui/material";
+import dynamic from "next/dynamic";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import ClusterSwitch from "./ClusterSwitch";
+import Filter from "./Filter";
+import Legend from "./Legend";
+import PointsOnMap from "./PointsOnMap";
+import DataTableFilterDataStructure from "@/core/utils/DataTableFilterDataStructure";
+import useProvinces from "@/lib/hooks/useProvince";
+import CustomSelectByDependency from "@/core/components/FilterDrawer/fieldsType/CustomSelectByDependency";
+import useEdaratLists from "@/lib/hooks/useEdaratLists";
+const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
+ loading: () => ,
+ ssr: false,
+});
+const typeOptions = [
+ { value: "", label: "همه نوع ها" },
+ { value: 0, label: "فصلی" },
+ { value: 1, label: "دائمی" },
+ { value: 2, label: "موقت" },
+];
+
+const MAX_POINTS = 1000;
+
+const containerStyles = {
+ width: "100%",
+ height: "100%",
+ p: 1,
+ border: "1px dashed",
+ borderColor: "divider",
+ borderRadius: 1,
+};
+
+const controlBarStyles = {
+ p: 1,
+ background: "#fff",
+};
+
+const RoadSafetyReportMap = () => {
+ const defaultValues = useMemo(
+ () => [
+ {
+ header: "استان",
+ id: "province_id",
+ filterMode: "equals",
+ datatype: "numeric",
+ value: "",
+ enable: true,
+ SelectComponent: (props) => {
+ const { provinces, errorProvinces, loadingProvinces } = useProvinces();
+ const getSelectOptions = useMemo(() => {
+ if (loadingProvinces) {
+ return [{ value: "loading", label: "در حال بارگذاری..." }];
+ }
+ if (errorProvinces) {
+ return [{ value: "error", label: "خطا در بارگذاری" }];
+ }
+ return [
+ { value: "", label: "کل کشور" },
+ ...provinces.map((province) => ({
+ value: province.id,
+ label: province.name_fa,
+ })),
+ ];
+ }, [provinces, errorProvinces, loadingProvinces]);
+ return (
+
+ );
+ },
+ },
+ {
+ eader: "اداره",
+ id: "edare_shahri_id",
+ filterMode: "equals",
+ datatype: "numeric",
+ dependencyId: "province_id",
+ value: "",
+ enable: true,
+ SelectComponent: (props) => {
+ const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
+ props.dependencyFieldValue.value
+ );
+ const [prevDependency, setPrevDependency] = useState(props.dependencyFieldValue.value);
+
+ const getSelectOptions = useMemo(() => {
+ if (props.dependencyFieldValue.value === "") {
+ return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
+ }
+ if (loadingEdaratList) {
+ return [{ value: "loading", label: "در حال بارگذاری..." }];
+ }
+ if (errorEdaratList) {
+ return [{ value: "error", label: "خطا در بارگذاری" }];
+ }
+ return [
+ { value: "", label: "کل ادارات" },
+ ...edaratList.map((edare) => ({
+ value: edare.id,
+ label: edare.name_fa,
+ })),
+ ];
+ }, [edaratList, loadingEdaratList, errorEdaratList]);
+ useEffect(() => {
+ if (prevDependency === props.dependencyFieldValue?.value) return;
+ props.handleChange({ ...props.filterParameters, value: "" });
+ setPrevDependency(props.dependencyFieldValue?.value);
+ }, [props.dependencyFieldValue?.value]);
+ return (
+
+ );
+ },
+ },
+ {
+ header: "نوع",
+ id: "type",
+ filterMode: "equals",
+ datatype: "numeric",
+ value: "",
+ enable: true,
+ selectOption: () => {
+ return typeOptions.map((status) => ({
+ value: status.value,
+ label: status.label,
+ }));
+ },
+ },
+ ],
+ []
+ );
+ const requestServer = useRequest();
+ const [filterData, setFilterData] = useState(
+ defaultValues.reduce((acc, item) => {
+ acc[item.id] = item;
+ return acc;
+ }, {})
+ );
+ const [isCluster, setCluster] = useState(false);
+ const [data, setData] = useState([]);
+ const [isLoading, setLoading] = useState(true);
+
+ const buildQueryParams = useCallback(() => {
+ const params = new URLSearchParams();
+ const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
+ params.set("filters", JSON.stringify(filters || []));
+ return params.toString();
+ }, [filterData]);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const params = new URLSearchParams();
+ const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
+ params.set("filters", JSON.stringify(filters || []));
+ const query = params.toString();
+ const response = await requestServer(`${GET_TOLL_HOUSE_MAP}?${query}`);
+ const fetchedData = response?.data?.data || [];
+
+ if (fetchedData.length > MAX_POINTS) {
+ setCluster(true);
+ }
+ setData(fetchedData);
+ } catch (error) {
+ console.log(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [filterData, buildQueryParams]);
+
+ return (
+
+
+
+
+ MAX_POINTS}
+ max={MAX_POINTS}
+ />
+
+
+
+
+
+
+
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+export default RoadSafetyReportMap;
diff --git a/src/core/utils/headerMenu.js b/src/core/utils/headerMenu.js
index 6faa5fa..dcadc5e 100644
--- a/src/core/utils/headerMenu.js
+++ b/src/core/utils/headerMenu.js
@@ -33,7 +33,7 @@ export const headerMenu = [
[
{
title: "پراکندگی بر روی نقشه راهدارخانه ها",
- href: api + "/v2/map?type=rahdar",
+ href: "/infrastructure/map",
},
{
title: "راهدارخانه ها",
diff --git a/src/core/utils/routes.js b/src/core/utils/routes.js
index 7e23042..11cf9b4 100644
--- a/src/core/utils/routes.js
+++ b/src/core/utils/routes.js
@@ -210,6 +210,7 @@ export const GET_TOLL_HOUSE_DETAILS = api + "/api/v3/road_maintenance_station";
export const DELETE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station";
export const UPDATE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station";
export const GET_TOLL_HOUSE_IMAGES = api + "/api/v3/road_maintenance_station/images";
+export const GET_TOLL_HOUSE_MAP = api + "/api/v3/road_maintenance_station/map";
// road missions
export const GET_ROAD_MISSIONS_OPERATOR_LIST = api + "/api/v3/missions/request_portal";