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" ? "فعال" : "غیرفعال"} + + + + + + + recognize_picture + + + + + + recognize_picture_second + + + + + ); +}; +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 + )} + + {open && } + + + ); +}; +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";