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/infrastructure/tollHouse/ShowProjectArea/ShowBound.jsx b/src/components/infrastructure/tollHouse/ShowProjectArea/ShowBound.jsx
new file mode 100644
index 0000000..24db476
--- /dev/null
+++ b/src/components/infrastructure/tollHouse/ShowProjectArea/ShowBound.jsx
@@ -0,0 +1,29 @@
+import { useEffect, useRef } from "react";
+import { FeatureGroup, useMap } from "react-leaflet";
+
+const ShowBound = ({ bound }) => {
+ const map = useMap();
+ const featureRef = useRef(null);
+
+ const safeFitBounds = (layer) => {
+ if (!layer || !map) return;
+ try {
+ const latLngs = layer.getLatLngs();
+ map.fitBounds(latLngs, {
+ paddingTopLeft: [20, 20],
+ paddingBottomRight: [20, 20],
+ });
+ } catch (e) {
+ console.warn("fitBounds failed:", e);
+ }
+ };
+
+ useEffect(() => {
+ bound?.editing?.disable();
+ featureRef.current.addLayer(bound);
+ safeFitBounds(bound);
+ }, []);
+
+ return ;
+};
+export default ShowBound;
diff --git a/src/components/infrastructure/tollHouse/ShowProjectArea/index.jsx b/src/components/infrastructure/tollHouse/ShowProjectArea/index.jsx
new file mode 100644
index 0000000..202ef61
--- /dev/null
+++ b/src/components/infrastructure/tollHouse/ShowProjectArea/index.jsx
@@ -0,0 +1,69 @@
+const { IconButton, Tooltip, Box, Dialog, DialogTitle, Typography, DialogContent } = require("@mui/material");
+import MapLayer from "@/core/components/MapLayer";
+import CloseIcon from "@mui/icons-material/Close";
+import LayersIcon from "@mui/icons-material/Layers";
+import L from "leaflet";
+import { useMemo, useState } from "react";
+import ShowBound from "./ShowBound";
+
+const ShowProjectArea = ({ primaryArea }) => {
+ const [openShowAreaDialog, setOpenShowAreaDialog] = useState(false);
+ const bound = useMemo(() => {
+ const coordinates = Object.values(primaryArea.coordinates);
+
+ const latLngCoords = coordinates.map(([lat, lng]) => [
+ parseFloat(lng),
+ parseFloat(lat),
+ ]);
+
+ return primaryArea.type === "polygon"
+ ? L.polygon(latLngCoords, { color: "#ff5555" })
+ : L.polyline(latLngCoords);
+ }, [primaryArea]);
+
+
+ return (
+
+
+ setOpenShowAreaDialog(true)}>
+
+
+
+
+
+ );
+};
+
+export default ShowProjectArea;
diff --git a/src/components/infrastructure/tollHouse/TollHouseList.jsx b/src/components/infrastructure/tollHouse/TollHouseList.jsx
index 11536b8..e8e6d2f 100644
--- a/src/components/infrastructure/tollHouse/TollHouseList.jsx
+++ b/src/components/infrastructure/tollHouse/TollHouseList.jsx
@@ -13,6 +13,7 @@ import { usePermissions } from "@/lib/hooks/usePermissions";
import LocationForm from "./RowActions/LocationForm";
import ImageDialog from "./RowActions/ImageForm";
import { useAuth } from "@/lib/contexts/auth";
+import ShowProjectArea from "./ShowProjectArea";
const statusOptions = [
{ value: "", label: "همه وضعیت ها" },
@@ -225,6 +226,28 @@ const TollHouseList = () => {
);
},
},
+ {
+ accessorKey: "area",
+ header: "نمایش محدوده طرح",
+ id: "area",
+ enableColumnFilter: true,
+ datatype: "text",
+ filterMode: "equals",
+ sortDescFirst: false,
+ columnFilterModeOptions: ["equals", "contains"],
+ grow: false,
+ size: 100,
+ Cell: ({ row }) =>
+ row.original.area ? (
+
+
+
+ ) : (
+ "-"
+ ),
+ },
{
accessorKey: "phone",
header: "تلفن راهدارخانه",
diff --git a/src/components/infrastructure/tollHouseMap/ClusterSwitch/index.jsx b/src/components/infrastructure/tollHouseMap/ClusterSwitch/index.jsx
new file mode 100644
index 0000000..e8fac87
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/ClusterSwitch/index.jsx
@@ -0,0 +1,25 @@
+import DoneIcon from "@mui/icons-material/Done";
+import { Chip, Tooltip, Zoom } from "@mui/material";
+
+const ClusterSwitch = ({ isCluster, setCluster, disabled, max }) => {
+ return (
+
+
+ setCluster((c) => !c)}
+ sx={{ borderRadius: 1 }}
+ icon={
+
+
+
+ }
+ />
+
+
+ );
+};
+export default ClusterSwitch;
diff --git a/src/components/infrastructure/tollHouseMap/Filter/index.jsx b/src/components/infrastructure/tollHouseMap/Filter/index.jsx
new file mode 100644
index 0000000..3cbbe5d
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/Filter/index.jsx
@@ -0,0 +1,44 @@
+"use client";
+import { Box, Button, Drawer } from "@mui/material";
+import FilterListIcon from "@mui/icons-material/FilterList";
+import { useCallback, useState } from "react";
+import FilterDrawer from "@/core/components/FilterDrawer";
+
+const drawerSx = {
+ overflowY: "hidden",
+ display: "flex",
+ flexDirection: "column",
+ height: "100%",
+ zIndex: "1300",
+};
+
+const boxSx = {
+ width: { xs: 300, sm: 450 },
+};
+
+const Filter = ({ filterData, setFilterData }) => {
+ const [open, setOpen] = useState(false);
+
+ const openDrawer = useCallback(() => setOpen(true), []);
+ const closeDrawer = useCallback(() => setOpen(false), []);
+
+ return (
+ <>
+ }>
+ فیلتر
+
+
+
+ {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..e0b3f16
--- /dev/null
+++ b/src/components/infrastructure/tollHouseMap/PointsOnMap/index.jsx
@@ -0,0 +1,79 @@
+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..2177397
--- /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";