Merge pull request #9 from witelgroup/feature/add_map_tollhouse
Feature/add map tollhouse
This commit is contained in:
@@ -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"
|
||||
@@ -7,6 +7,10 @@ const nextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: process.env.HOST,
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: process.env.HOST_RMTO,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
import PageLoading from "@/core/components/PageLoading";
|
||||
|
||||
const Loading = () => {
|
||||
return <PageLoading />;
|
||||
};
|
||||
export default Loading;
|
||||
@@ -0,0 +1,16 @@
|
||||
import WithPermission from "@/core/middlewares/withPermission";
|
||||
import TollHouseMapPage from "@/components/infrastructure/tollHouseMap";
|
||||
|
||||
export const metadata = {
|
||||
title: "پراکندگی بر روی نقشه راهدارخانه ها",
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<WithPermission permission_name={["show-tollhouse", "show-tollhouse-province"]}>
|
||||
<TollHouseMapPage />
|
||||
</WithPermission>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -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 }) => {
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Controller
|
||||
control={control}
|
||||
name={"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>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}>
|
||||
|
||||
@@ -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 }) => {
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{driver && (
|
||||
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||
<LtrTextField
|
||||
autoComplete="off"
|
||||
type="tel"
|
||||
value={kilometer}
|
||||
onChange={(e) => {
|
||||
if (isNaN(Number(e.target.value))) return;
|
||||
setKilometer(e.target.value);
|
||||
}}
|
||||
size="small"
|
||||
fullWidth
|
||||
label="کیلومتر خودرو"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Stack spacing={1}>
|
||||
@@ -208,10 +189,10 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
|
||||
</Stack>
|
||||
<Box>
|
||||
<Button
|
||||
onClick={() => handleSubmit(machine, driver, kilometer)}
|
||||
onClick={() => handleSubmit(machine, driver)}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!machine || !driver || !kilometer || isSubmitting}
|
||||
disabled={!machine || !driver || isSubmitting}
|
||||
autoFocus
|
||||
>
|
||||
تایید و تخصیص
|
||||
|
||||
@@ -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 <FeatureGroup ref={featureRef} />;
|
||||
};
|
||||
export default ShowBound;
|
||||
@@ -0,0 +1,65 @@
|
||||
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 (
|
||||
<Box>
|
||||
<Tooltip title="نمایش پلیگان" placement="right" arrow>
|
||||
<IconButton size="small" color="primary" onClick={() => setOpenShowAreaDialog(true)}>
|
||||
<LayersIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog
|
||||
open={openShowAreaDialog}
|
||||
onClose={() => setOpenShowAreaDialog(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
transition: "all .3s",
|
||||
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||
borderRadius: 2,
|
||||
padding: 1,
|
||||
},
|
||||
}}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
dir="rtl"
|
||||
>
|
||||
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
|
||||
محدوده طرح
|
||||
</Typography>
|
||||
<IconButton onClick={() => setOpenShowAreaDialog(false)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ width: "100%", height: "400px" }}>
|
||||
<MapLayer style={{ borderRadius: "4px" }}>
|
||||
<ShowBound bound={bound} />
|
||||
</MapLayer>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowProjectArea;
|
||||
@@ -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,26 @@ 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 ? (
|
||||
<Stack alignItems={"center"} justifyContent={"center"}>
|
||||
<ShowProjectArea primaryArea={row.original.area} />
|
||||
</Stack>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "phone",
|
||||
header: "تلفن راهدارخانه",
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip title={disabled ? `بیشتر از ${max} تا فعالیت باید حالت کلاستر باشد` : ""}>
|
||||
<span>
|
||||
<Chip
|
||||
label={"حالت کلاستر"}
|
||||
variant="outlined"
|
||||
disabled={disabled}
|
||||
color={isCluster ? "success" : "default"}
|
||||
onClick={() => setCluster((c) => !c)}
|
||||
sx={{ borderRadius: 1 }}
|
||||
icon={
|
||||
<Zoom in={isCluster}>
|
||||
<DoneIcon />
|
||||
</Zoom>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
export default ClusterSwitch;
|
||||
44
src/components/infrastructure/tollHouseMap/Filter/index.jsx
Normal file
44
src/components/infrastructure/tollHouseMap/Filter/index.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Button onClick={openDrawer} variant="outlined" size="small" endIcon={<FilterListIcon />}>
|
||||
فیلتر
|
||||
</Button>
|
||||
<Drawer open={open} onClose={closeDrawer} sx={drawerSx}>
|
||||
<Box sx={boxSx}>
|
||||
{open && (
|
||||
<FilterDrawer
|
||||
defaultValues={filterData}
|
||||
setFilterData={setFilterData}
|
||||
closeDrawer={closeDrawer}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Filter;
|
||||
35
src/components/infrastructure/tollHouseMap/Legend/index.jsx
Normal file
35
src/components/infrastructure/tollHouseMap/Legend/index.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
const Legend = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack direction={"row"} spacing={1} alignItems={"center"}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
border: 1,
|
||||
borderRadius: "50%",
|
||||
borderColor: (theme) => theme.palette.success.main,
|
||||
background: (theme) => theme.palette.success.light,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption">فعال</Typography>
|
||||
</Stack>
|
||||
<Stack direction={"row"} spacing={1} alignItems={"center"}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
border: 1,
|
||||
borderRadius: "50%",
|
||||
borderColor: (theme) => theme.palette.error.main,
|
||||
background: (theme) => theme.palette.error.light,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption">غیرفعال</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default Legend;
|
||||
@@ -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 (
|
||||
<Stack spacing={2}>
|
||||
<Stack direction={"row"} alignItems={"center"}>
|
||||
<Chip size="small" label="کد یکتا" />
|
||||
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>{data.id !== "" && data.id}</Typography>
|
||||
</Stack>
|
||||
<Stack direction={"row"} alignItems={"center"}>
|
||||
<Chip size="small" label="استان" />
|
||||
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
|
||||
{data.province_name !== "" && data.province_name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction={"row"} alignItems={"center"}>
|
||||
<Chip size="small" label="نوع" />
|
||||
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
|
||||
{data.type_fa !== "" && data.type_fa}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction={"row"} alignItems={"center"}>
|
||||
<Chip size="small" label="وضعیت" />
|
||||
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
|
||||
{data.status === "1" ? "فعال" : "غیرفعال"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
|
||||
spacing={1}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<Chip size="small" label="تصویر بازدید" />
|
||||
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
|
||||
<Image
|
||||
src={data.files[0].path ?? ""}
|
||||
style={{ objectFit: "contain" }}
|
||||
alt="recognize_picture"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
|
||||
spacing={1}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<Chip size="small" label="تصویر بازدید" />
|
||||
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
|
||||
<Image
|
||||
src={data.files[1].path ?? ""}
|
||||
style={{ objectFit: "contain" }}
|
||||
alt="recognize_picture_second"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default ContentInfoItem;
|
||||
@@ -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 (
|
||||
<>
|
||||
<DialogTitle>جزئیات فعالیت</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{loading ? <DialogLoading /> : data && <ContentInfoItem data={data} />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog} variant="outlined" color="secondary" autoFocus>
|
||||
بستن
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default DialogInfoItem;
|
||||
@@ -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 }) => (
|
||||
<CircleMarker
|
||||
key={id}
|
||||
center={L.latLng(Number(lat), Number(lng))}
|
||||
radius={8}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
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: `<div class="custom-marker-cluster">${count}</div>`,
|
||||
className: "custom-marker-cluster",
|
||||
iconSize: L.point(40, 40, true),
|
||||
});
|
||||
};
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCluster ? (
|
||||
<MarkerClusterGroup iconCreateFunction={createClusterCustomIcon} showCoverageOnHover={false}>
|
||||
{markers}
|
||||
</MarkerClusterGroup>
|
||||
) : (
|
||||
markers
|
||||
)}
|
||||
<Dialog open={open} onClose={closeDialog} fullWidth maxWidth="sm">
|
||||
{open && <DialogInfoItem selectedId={selectedId} closeDialog={closeDialog} />}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default PointsOnMap;
|
||||
231
src/components/infrastructure/tollHouseMap/index.jsx
Normal file
231
src/components/infrastructure/tollHouseMap/index.jsx
Normal file
@@ -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: () => <MapLoading />,
|
||||
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 (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={loadingProvinces ? "loading" : props.filterParameters.value}
|
||||
selectOption={getSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={
|
||||
props.dependencyFieldValue?.value === ""
|
||||
? "empty"
|
||||
: loadingEdaratList
|
||||
? "loading"
|
||||
: props.filterParameters.value
|
||||
}
|
||||
selectOption={getSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Box sx={containerStyles}>
|
||||
<MapLayer style={{ borderRadius: "4px" }}>
|
||||
<Stack
|
||||
direction="row-reverse"
|
||||
sx={{ position: "absolute", top: 0, right: 0, width: "100%", zIndex: 1000 }}
|
||||
>
|
||||
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, borderBottomLeftRadius: "4px" }}>
|
||||
<ClusterSwitch
|
||||
isCluster={isCluster}
|
||||
setCluster={setCluster}
|
||||
disabled={data.length > MAX_POINTS}
|
||||
max={MAX_POINTS}
|
||||
/>
|
||||
<Filter filterData={filterData} setFilterData={setFilterData} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row-reverse"
|
||||
sx={{ position: "absolute", bottom: 0, right: 0, width: "100%", zIndex: 1000 }}
|
||||
>
|
||||
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, pb: 0, borderTopLeftRadius: "4px" }}>
|
||||
<Legend />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{isLoading && (
|
||||
<Box sx={{ position: "relative", width: "100%", height: "100%", opacity: 0.7, zIndex: 999 }}>
|
||||
<LoadingHardPage width={80} height={80} loading={isLoading} sx={{ position: "absolute" }} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<PointsOnMap data={data} isCluster={isCluster} />
|
||||
</MapLayer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default RoadSafetyReportMap;
|
||||
@@ -33,7 +33,7 @@ export const headerMenu = [
|
||||
[
|
||||
{
|
||||
title: "پراکندگی بر روی نقشه راهدارخانه ها",
|
||||
href: api + "/v2/map?type=rahdar",
|
||||
href: "/infrastructure/map",
|
||||
},
|
||||
{
|
||||
title: "راهدارخانه ها",
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user