diff --git a/package.json b/package.json index c5b2139..f5c7205 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-hook-form": "^7.52.1", "react-leaflet": "^4.2.1", "react-leaflet-draw": "^0.20.4", + "react-leaflet-markercluster": "^4.2.1", "react-toastify": "^10.0.5", "sass": "^1.75.0", "sharp": "^0.33.4", diff --git a/src/app/(withAuth)/dashboard/road-items/reports/map/page.js b/src/app/(withAuth)/dashboard/road-items/reports/map/page.js new file mode 100644 index 0000000..00f000d --- /dev/null +++ b/src/app/(withAuth)/dashboard/road-items/reports/map/page.js @@ -0,0 +1,6 @@ +import RoadItemsReportMap from "@/components/dashboard/roadItems/reports/Map"; + +const Page = () => { + return ; +}; +export default Page; diff --git a/src/assets/scss/global.scss b/src/assets/scss/global.scss index 7b73e7e..1fc9f82 100644 --- a/src/assets/scss/global.scss +++ b/src/assets/scss/global.scss @@ -1,6 +1,7 @@ @import "fontiran"; @import "leaflet/dist/leaflet.css"; @import "react-toastify/dist/ReactToastify.css"; +@import "./map-styles.scss"; .filter-toast { box-shadow: diff --git a/src/assets/scss/map-styles.scss b/src/assets/scss/map-styles.scss index 44e6651..595f664 100644 --- a/src/assets/scss/map-styles.scss +++ b/src/assets/scss/map-styles.scss @@ -1,27 +1,9 @@ -.leaflet-draw-tooltip { - background-color: #4f4f4f; - font-family: IRANSans; +.custom-marker-cluster { + font-family: IRANSansFaNum; + background-color: rgba(51, 181, 229, 0.6); color: #fff; - font-size: 10px; - padding: 2px 8px; - margin-right: 15px; - border-radius: 4px; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); -} - -.leaflet-draw-tooltip:before { - content: ""; - position: absolute; - bottom: 100%; - left: 103%; - transform: rotate(90deg); - margin-left: -6px; - border-width: 6px; - border-style: solid; - border-color: transparent transparent #333 transparent; -} - -.leaflet-draw-tooltip-subtext { - font-size: 12px; - color: #ffeb3b; + border-radius: 50%; + text-align: center; + font-size: 14px; + line-height: 40px; } diff --git a/src/components/dashboard/inquiryPrivacy/provinceAdmin/zaminGov/RowActions/MapModify/PolygonForDesign.jsx b/src/components/dashboard/inquiryPrivacy/provinceAdmin/zaminGov/RowActions/MapModify/PolygonForDesign.jsx index cd75f8d..4e1bb43 100644 --- a/src/components/dashboard/inquiryPrivacy/provinceAdmin/zaminGov/RowActions/MapModify/PolygonForDesign.jsx +++ b/src/components/dashboard/inquiryPrivacy/provinceAdmin/zaminGov/RowActions/MapModify/PolygonForDesign.jsx @@ -1,7 +1,5 @@ "use client"; -import "leaflet-draw/dist/leaflet.draw.css"; -import "@/assets/scss/map-styles.scss"; import { FeatureGroup, useMap } from "react-leaflet"; import { EditControl } from "react-leaflet-draw"; import { useEffect, useRef, useState } from "react"; diff --git a/src/components/dashboard/roadItems/operator/OperatorList.jsx b/src/components/dashboard/roadItems/operator/OperatorList.jsx index 52913cc..33b37c6 100644 --- a/src/components/dashboard/roadItems/operator/OperatorList.jsx +++ b/src/components/dashboard/roadItems/operator/OperatorList.jsx @@ -300,7 +300,7 @@ const OperatorList = () => { }, { accessorKey: "status", - header: "وضعیت", // Status + header: "وضعیت نظارت", id: "status", enableColumnFilter: true, datatype: "numeric", @@ -318,7 +318,7 @@ const OperatorList = () => { }, { accessorKey: "supervisor_description", - header: "توضیحات کارشناس", // Description + header: "توضیحات ناظر", id: "supervisor_description", enableColumnFilter: false, enableSorting: false, diff --git a/src/components/dashboard/roadItems/operator/RowActions/DescriptionForm/index.jsx b/src/components/dashboard/roadItems/operator/RowActions/DescriptionForm/index.jsx index 481e2d6..91b135a 100644 --- a/src/components/dashboard/roadItems/operator/RowActions/DescriptionForm/index.jsx +++ b/src/components/dashboard/roadItems/operator/RowActions/DescriptionForm/index.jsx @@ -8,7 +8,7 @@ const DescriptionForm = ({ description }) => { return ( <> - + setOpenOfficerDescriptionDialog(true)}> @@ -30,7 +30,7 @@ const DescriptionForm = ({ description }) => { > - توضیحات کارشناس + توضیحات ناظر setOpenOfficerDescriptionDialog(false)} size="small"> diff --git a/src/components/dashboard/roadItems/reports/Map/ClusterSwitch/index.jsx b/src/components/dashboard/roadItems/reports/Map/ClusterSwitch/index.jsx new file mode 100644 index 0000000..e8fac87 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/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/dashboard/roadItems/reports/Map/Filter/Drawer/ActivityDateTimeFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ActivityDateTimeFilter.jsx new file mode 100644 index 0000000..cab7ddc --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ActivityDateTimeFilter.jsx @@ -0,0 +1,32 @@ +import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; +import { Box } from "@mui/material"; + +const ActivityDateTimeFilter = ({ value, onChange, error }) => { + return ( + + { + onChange([formattedDate, value[1]]); + }} + maxDate={value[1]} + placeholder={`از تاریخ`} + /> + { + onChange([value[0], formattedDate]); + }} + minDate={value[0]} + placeholder={`تا تاریخ`} + helperText={error ? error.message : null} + error={Boolean(error)} + /> + + ); +}; +export default ActivityDateTimeFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/EdarehShahriFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/EdarehShahriFilter.jsx new file mode 100644 index 0000000..eb08e27 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/EdarehShahriFilter.jsx @@ -0,0 +1,65 @@ +"use client"; +import useEdaratLists from "@/lib/hooks/useEdaratLists"; +import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material"; +import { useEffect, useMemo, useState } from "react"; +import { useWatch } from "react-hook-form"; + +const EdarehShahriFilter = ({ control, value, onChange }) => { + const dependencyField = useWatch({ control, name: "province_id" }); + return ; +}; + +const EdarehShahriController = ({ value, onChange, dependencyField }) => { + const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(dependencyField); + const [prevDependency, setPrevDependency] = useState(dependencyField); + + const columnSelectOption = useMemo(() => { + if (dependencyField === "") { + 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 === dependencyField) return; + onChange(""); + setPrevDependency(dependencyField); + }, [dependencyField]); + + return ( + + + اداره + + + + ); +}; +export default EdarehShahriFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ItemFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ItemFilter.jsx new file mode 100644 index 0000000..de27d11 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ItemFilter.jsx @@ -0,0 +1,47 @@ +import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems"; +import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material"; +import { useMemo } from "react"; + +const ItemFilter = ({ value, onChange }) => { + const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems(); + const columnSelectOption = useMemo(() => { + if (loadingItemsList) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorItemsList) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "همه آیتم ها" }, + ...itemsList.map((item) => ({ + value: item.id, + label: item.name, + })), + ]; + }, [itemsList, loadingItemsList, errorItemsList]); + + return ( + + + آیتم فعالیت + + + + ); +}; +export default ItemFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ProvinceFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ProvinceFilter.jsx new file mode 100644 index 0000000..4de3f9e --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/ProvinceFilter.jsx @@ -0,0 +1,47 @@ +import useProvinces from "@/lib/hooks/useProvince"; +import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material"; +import { useMemo } from "react"; + +const ProvinceFilter = ({ value, onChange }) => { + const { provinces, errorProvinces, loadingProvinces } = useProvinces(); + const columnSelectOption = 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 ( + + + استان + + + + ); +}; +export default ProvinceFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/StatusFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/StatusFilter.jsx new file mode 100644 index 0000000..dd52ea5 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/StatusFilter.jsx @@ -0,0 +1,35 @@ +import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material"; + +const StatusFilter = ({ value, onChange }) => { + const statusOptions = [ + { value: "", label: "همه وضعیت ها" }, + { value: 0, label: "درحال بررسی" }, + { value: 1, label: "تایید" }, + { value: 2, label: "عدم تایید" }, + ]; + + return ( + + + وضعیت نظارت + + + + ); +}; +export default StatusFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/SubItemFilter.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/SubItemFilter.jsx new file mode 100644 index 0000000..e9f973a --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/SubItemFilter.jsx @@ -0,0 +1,65 @@ +"use client"; +import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems"; +import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material"; +import { useEffect, useMemo, useState } from "react"; +import { useWatch } from "react-hook-form"; + +const SubItemFilter = ({ control, value, onChange }) => { + const dependencyField = useWatch({ control, name: "item_id" }); + return ; +}; + +const SubItemController = ({ value, onChange, dependencyField }) => { + const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(dependencyField); + const [prevDependency, setPrevDependency] = useState(dependencyField); + + const columnSelectOption = useMemo(() => { + if (dependencyField === "") { + return [{ value: "empty", label: "ابتدا آیتم فعالیت را انتخاب کنید" }]; + } + if (loadingSubItemsList) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorSubItemsList) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "همه اقدام ها" }, + ...subItemsList.map((item) => ({ + value: item.sub_item, + label: item.name, + })), + ]; + }, [subItemsList, loadingSubItemsList, errorSubItemsList]); + + useEffect(() => { + if (prevDependency === dependencyField) return; + onChange(""); + setPrevDependency(dependencyField); + }, [dependencyField]); + + return ( + + + اقدام انجام شده + + + + ); +}; +export default SubItemFilter; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/index.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/index.jsx new file mode 100644 index 0000000..9700220 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Filter/Drawer/index.jsx @@ -0,0 +1,141 @@ +import ScrollBox from "@/core/components/ScrollBox"; +import { Box, Button, IconButton, Typography } from "@mui/material"; +import CancelIcon from "@mui/icons-material/Cancel"; +import FilterAltIcon from "@mui/icons-material/FilterAlt"; +import { Controller, useForm } from "react-hook-form"; +import ProvinceFilter from "./ProvinceFilter"; +import EdarehShahriFilter from "./EdarehShahriFilter"; +import ItemFilter from "./ItemFilter"; +import SubItemFilter from "./SubItemFilter"; +import StatusFilter from "./StatusFilter"; +import ActivityDateTimeFilter from "./ActivityDateTimeFilter"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as Yup from "yup"; + +const validationSchema = Yup.object({ + activity_date_time: Yup.array() + .of(Yup.string().nullable()) + .test({ + test(value, ctx) { + const [start, end] = value || ["", ""]; + if ((start && !end) || (!start && end)) { + return ctx.createError({ message: "این بخش را تکمیل نمایید" }); + } + return true; + }, + }), +}); + +const headerSx = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + px: 2, + py: 1, + backgroundColor: "#155175", + boxShadow: "rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px", + maxWidth: "450px", +}; + +const headerTitleSx = { display: "flex", alignItems: "center" }; +const headerIconSx = { color: "#fff", mr: 1 }; +const iconButtonSx = { color: "#fff" }; + +const formContainerSx = { px: 2, py: 3 }; +const footerSx = { display: "flex", justifyContent: "center", alignItems: "center", pb: 2 }; + +const submitButtonSx = { + px: 8, + boxShadow: "rgba(0, 0, 0, 0.23) 0px 6px 6px, rgba(0, 0, 0, 0.19) 10px 0px 20px", + backgroundColor: "primary2", + ":hover": { backgroundColor: "primary2" }, +}; + +const FilterDrawer = ({ defaultValues, setFilterData, closeDrawer }) => { + const { + control, + handleSubmit, + formState: { isDirty }, + } = useForm({ + defaultValues, + resolver: yupResolver(validationSchema), + mode: "onBlur", + }); + + const onSubmit = (data) => { + setFilterData(data); + closeDrawer(); + }; + + return ( + <> + + + + + فیلتر + + + + + + + + +
+ + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + + + +
+
+ + ); +}; +export default FilterDrawer; diff --git a/src/components/dashboard/roadItems/reports/Map/Filter/index.jsx b/src/components/dashboard/roadItems/reports/Map/Filter/index.jsx new file mode 100644 index 0000000..c12a511 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/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 "./Drawer"; + +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/dashboard/roadItems/reports/Map/Legend/index.jsx b/src/components/dashboard/roadItems/reports/Map/Legend/index.jsx new file mode 100644 index 0000000..20707aa --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/Legend/index.jsx @@ -0,0 +1,48 @@ +import { Box, Stack, Typography } from "@mui/material"; + +const Legend = () => { + return ( + + + theme.palette.primary.main, + background: (theme) => theme.palette.primary.light, + }} + /> + در حال بررسی + + + theme.palette.success.main, + background: (theme) => theme.palette.success.light, + }} + /> + تایید شده + + + theme.palette.warning.main, + background: (theme) => theme.palette.warning.light, + }} + /> + عدم تایید + + + ); +}; +export default Legend; diff --git a/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx new file mode 100644 index 0000000..a10a1aa --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/ContentInfoItem.jsx @@ -0,0 +1,104 @@ +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_fa !== "" && data.province_fa} + + + + + + + {data.edarat_name !== "" && data.edarat_name} + + + + + + + {data.item_fa !== "" && data.item_fa} + + + + + + + {data.sub_item_fa !== "" && data.sub_item_fa} + + + + + + + + {(data.sub_item_data / 1).toLocaleString()} + + + {`(${data.unit_fa})`} + + + + + + + + {data.activity_date_time && + moment(data.activity_date_time).locale("fa").format("HH:mm | yyyy/MM/DD")} + + + + + + + {data.created_at && moment(data.created_at).locale("fa").format("HH:mm | yyyy/MM/DD")} + + + + + + + {data.status_fa !== "" && data.status_fa} + + + + + + + before_image + + + + + + before_image + + + + + ); +}; +export default ContentInfoItem; diff --git a/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/index.jsx b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/index.jsx new file mode 100644 index 0000000..a3c9b23 --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/DialogInfoItem/index.jsx @@ -0,0 +1,51 @@ +import DialogLoading from "@/core/components/DialogLoading"; +import { GET_ROAD_ITEM } 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_ROAD_ITEM}/${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/dashboard/roadItems/reports/Map/PointsOnMap/index.jsx b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/index.jsx new file mode 100644 index 0000000..8599f3f --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/PointsOnMap/index.jsx @@ -0,0 +1,71 @@ +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 === 0) return theme.palette.info.main; + if (status === 1) return theme.palette.success.main; + return theme.palette.warning.main; + }; + + const markers = useMemo(() => { + return data.map(({ i, l, g, status }) => ( + { + openDialog(); + setSelectedId(i); + }, + }} + key={i} + center={L.latLng(l, g)} + radius={8} + color={getMarkerColor(status)} + /> + )); + }, [data, theme]); + + useEffect(() => { + if (data.length === 0) return; + const bounds = L.latLngBounds(data.map(({ l, g }) => L.latLng(l, g))); + 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/dashboard/roadItems/reports/Map/index.jsx b/src/components/dashboard/roadItems/reports/Map/index.jsx new file mode 100644 index 0000000..1daacfb --- /dev/null +++ b/src/components/dashboard/roadItems/reports/Map/index.jsx @@ -0,0 +1,127 @@ +"use client"; +import LoadingHardPage from "@/core/components/LoadingHardPage"; +import MapLoading from "@/core/components/MapLayer/Loading"; +import { GET_ROAD_ITEMS_REPORT_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, useState } from "react"; +import Filter from "./Filter"; +import PointsOnMap from "./PointsOnMap"; +import moment from "jalali-moment"; +import ClusterSwitch from "./ClusterSwitch"; +import Legend from "./Legend"; +const MapLayer = dynamic(() => import("@/core/components/MapLayer"), { + loading: () => , + ssr: false, +}); + +const defaultValues = { + province_id: "", + edarat_id: "", + item_id: "", + sub_item_id: "", + status: "", + activity_date_time: [moment().locale("en").format("YYYY-MM-DD"), moment().locale("en").format("YYYY-MM-DD")], +}; + +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 RoadItemsReportMap = () => { + const requestServer = useRequest(); + const [filterData, setFilterData] = useState(defaultValues); + const [isCluster, setCluster] = useState(false); + const [data, setData] = useState([]); + const [isLoading, setLoading] = useState(true); + + const buildQueryParams = useCallback(() => { + const params = new URLSearchParams(); + const { province_id, edarat_id, item_id, sub_item_id, activity_date_time, status } = filterData; + + if (province_id) params.set("province_id", province_id); + if (edarat_id) params.set("edarat_id", edarat_id); + if (item_id) params.set("item_id", item_id); + if (sub_item_id) params.set("sub_item_id", sub_item_id); + if (activity_date_time[0]) { + params.set("date_from", activity_date_time[0]); + params.set("date_to", activity_date_time[1]); + } + if (status) params.set("status", status); + + return params.toString(); + }, [filterData]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const query = buildQueryParams(); + const response = await requestServer(`${GET_ROAD_ITEMS_REPORT_MAP}?${query}`); + const fetchedData = response?.data?.data || []; + + if (fetchedData.length > MAX_POINTS) { + setCluster(true); + } + setData(fetchedData); + } catch (error) { + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [filterData, buildQueryParams]); + + return ( + + + + + MAX_POINTS} + max={MAX_POINTS} + /> + + + + + + + + + + + {isLoading && ( + + + + )} + + + + + ); +}; +export default RoadItemsReportMap; diff --git a/src/components/dashboard/roadItems/supervisor/SupervisorList.jsx b/src/components/dashboard/roadItems/supervisor/SupervisorList.jsx index 84a48c9..f9a7d78 100644 --- a/src/components/dashboard/roadItems/supervisor/SupervisorList.jsx +++ b/src/components/dashboard/roadItems/supervisor/SupervisorList.jsx @@ -404,7 +404,7 @@ const SupervisorList = () => { }, { accessorKey: "status", - header: "وضعیت", // Status + header: "وضعیت نظارت", id: "status", enableColumnFilter: true, datatype: "numeric", diff --git a/src/core/components/DataTable/filter/FilterBody.jsx b/src/core/components/DataTable/filter/FilterBody.jsx index ab12820..461eb01 100644 --- a/src/core/components/DataTable/filter/FilterBody.jsx +++ b/src/core/components/DataTable/filter/FilterBody.jsx @@ -1,32 +1,15 @@ "use client"; -import FilterBodyField from "@/core/components/DataTable/filter/FilterBodyField"; +import FilterBodyControllerWithDependency from "@/core/components/DataTable/filter/FilterBodyControllerWithDependency"; import FilterHeader from "@/core/components/DataTable/filter/FilterHeader"; import useDataTable from "@/lib/hooks/useDataTable"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Box, Button, Drawer, styled } from "@mui/material"; +import { Box, Button } from "@mui/material"; import { useEffect, useState } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; +import { useForm } from "react-hook-form"; import * as Yup from "yup"; +import ScrollBox from "../../ScrollBox"; import FilterBodyController from "./FilterBodyController"; -import FilterBodyControllerWithDependency from "@/core/components/DataTable/filter/FilterBodyControllerWithDependency"; - -const ScrollBox = styled(Box)({ - flexGrow: 1, - overflowY: "scroll", - maxWidth: "450px", - "::-webkit-scrollbar": { - width: "5px", - }, - "::-webkit-scrollbar-track": { - boxShadow: "inset 0 0 5px #fff", - borderRadius: "5px", - }, - "::-webkit-scrollbar-thumb": { - background: "#155175", - borderRadius: "0px", - }, -}); function FilterBody({ columns, drawerState, setDrawerState }) { const { filterData, setFilterData } = useDataTable(); @@ -89,7 +72,7 @@ function FilterBody({ columns, drawerState, setDrawerState }) { {Object.keys(filterData).length > 0 && (
- + {columns.map((column) => column.enableColumnFilter ? ( column.dependencyId ? ( diff --git a/src/core/components/DataTable/filter/index.jsx b/src/core/components/DataTable/filter/index.jsx index b922bc4..5885cb4 100644 --- a/src/core/components/DataTable/filter/index.jsx +++ b/src/core/components/DataTable/filter/index.jsx @@ -2,7 +2,7 @@ import { useState } from "react"; import FilterButton from "@/core/components/DataTable/filter/FilterButton"; import FilterBody from "@/core/components/DataTable/filter/FilterBody"; -import { Drawer } from "@mui/material"; +import { Box, Drawer } from "@mui/material"; function FilterColumn({ columns, user_id, page_name, table_name }) { const [open, setOpen] = useState(false); @@ -15,14 +15,18 @@ function FilterColumn({ columns, user_id, page_name, table_name }) { onClose={() => setOpen(false)} sx={{ overflowY: "hidden", display: "flex", flexDirection: "column", height: "100%", zIndex: "1300" }} > - + + {open && ( + + )} + ); diff --git a/src/core/components/LoadingHardPage.jsx b/src/core/components/LoadingHardPage.jsx index 89e5f17..e0ff917 100644 --- a/src/core/components/LoadingHardPage.jsx +++ b/src/core/components/LoadingHardPage.jsx @@ -6,15 +6,12 @@ import SvgError from "@/core/components/svgs/SvgError"; const LoadingImage = styled(Box)({ "@keyframes load": { "0%": { - // opacity: 0, transform: "scale(1)", }, "50%": { - // opacity: 1, transform: "scale(.5)", }, "100%": { - // opacity: 0, transform: "scale(1)", }, }, diff --git a/src/core/components/MapInfoOneMarker.jsx b/src/core/components/MapInfoOneMarker.jsx index b42ced9..b9574c1 100644 --- a/src/core/components/MapInfoOneMarker.jsx +++ b/src/core/components/MapInfoOneMarker.jsx @@ -38,9 +38,9 @@ const createCustomIcon = (size, iconUrl, labelText, color) => { const MAX_ZOOM_FOR_MARKER = 13; const MapInteraction = ({ setValue, startLat, startLng }) => { - const [isMarkerLocked, setIsMarkerLocked] = useState(!!(startLat && startLng)); // وضعیت قفل مارکر + const [isMarkerLocked, setIsMarkerLocked] = useState(!!(startLat && startLng)); const [enableSend, setEnableSend] = useState(false); - const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); // رنگ آیکن شروع + const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); const [markerPosition, setMarkerPosition] = useState( startLat && startLng ? { lat: startLat, lng: startLng } : null ); diff --git a/src/core/components/MapInfoTwoMarker.jsx b/src/core/components/MapInfoTwoMarker.jsx index 44745b1..9b114d3 100644 --- a/src/core/components/MapInfoTwoMarker.jsx +++ b/src/core/components/MapInfoTwoMarker.jsx @@ -37,13 +37,13 @@ const createCustomIcon = (size, iconUrl, labelText, color) => { const MAX_ZOOM_FOR_MARKER = 13; const MapInteraction = ({ setValue, startLat, startLng, endLat, endLng }) => { - const [isStartLocked, setIsStartLocked] = useState(!!(startLat && startLng)); // وضعیت قفل نقطه آغاز - const [isEndLocked, setIsEndLocked] = useState(!!(endLat && endLng)); // وضعیت قفل نقطه پایان + const [isStartLocked, setIsStartLocked] = useState(!!(startLat && startLng)); + const [isEndLocked, setIsEndLocked] = useState(!!(endLat && endLng)); const [startPosition, setStartPosition] = useState(startLat && startLng ? { lat: startLat, lng: startLng } : null); const [endPosition, setEndPosition] = useState(endLat && endLng ? { lat: endLat, lng: endLng } : null); const [enableSend, setEnableSend] = useState(false); - const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); // رنگ آیکن شروع - const [endIconColor, setEndIconColor] = useState(endLat ? "#D13131" : "#003d4f"); // رنگ آیکن پایان + const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); + const [endIconColor, setEndIconColor] = useState(endLat ? "#D13131" : "#003d4f"); const startRef = useRef(); const endRef = useRef(); diff --git a/src/core/components/ScrollBox.jsx b/src/core/components/ScrollBox.jsx new file mode 100644 index 0000000..11f8404 --- /dev/null +++ b/src/core/components/ScrollBox.jsx @@ -0,0 +1,19 @@ +import { Box, styled } from "@mui/material"; + +const ScrollBox = styled(Box)({ + flexGrow: 1, + overflowY: "auto", + maxWidth: "450px", + "::-webkit-scrollbar": { + width: "5px", + }, + "::-webkit-scrollbar-track": { + boxShadow: "inset 0 0 5px #fff", + borderRadius: "5px", + }, + "::-webkit-scrollbar-thumb": { + background: "#155175", + borderRadius: "0px", + }, +}); +export default ScrollBox; diff --git a/src/core/utils/pageMenu.js b/src/core/utils/pageMenu.js index f312523..fa593bb 100644 --- a/src/core/utils/pageMenu.js +++ b/src/core/utils/pageMenu.js @@ -137,7 +137,7 @@ export const pageMenu = [ id: "roadItemManagmentMap", label: "پراکندگی بر روی نقشه", type: "page", - route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_items", + route: "/dashboard/road-items/reports/map", icon: , permissions: [ "show-road-item-supervise-cartable", diff --git a/src/core/utils/routes.js b/src/core/utils/routes.js index 2e5a8f8..94ce0d9 100644 --- a/src/core/utils/routes.js +++ b/src/core/utils/routes.js @@ -69,6 +69,8 @@ export const RESTORE_BY_SUPERVISOR = api + "/api/v3/road_items/restore"; export const GET_CAR_LIST_SEARCH = api + "/api/v3/cmms_machines/search"; export const GET_RAHDARANS_LIST_SEARCH = api + "/api/v3/rahdaran/search"; export const GET_OBSERVED_GASHT_LIST = api + "/api/v3/observed_items/filter"; +export const GET_ROAD_ITEMS_REPORT_MAP = api + "/api/v3/road_item_reports/activities_on_map"; +export const GET_ROAD_ITEM = api + "/api/v3/road_item_reports/show_on_map"; export const GET_PROVINCE_ACTIVITY_PER_ITEM = api + "/api/v3/road_item_reports/country_activity_per_item"; export const GET_CITY_ACTIVITY_PER_ITEM = api + "/api/v3/road_item_reports/province_activity_per_item"; export const GET_PROVINCE_ACTIVITY_PER_SUB_ITEM = api + "/api/v3/road_item_reports/country_activity_per_sub_item"; diff --git a/src/lib/hooks/useRoadItemGetISubtems.js b/src/lib/hooks/useRoadItemGetISubtems.js index b0e3264..368f72d 100644 --- a/src/lib/hooks/useRoadItemGetISubtems.js +++ b/src/lib/hooks/useRoadItemGetISubtems.js @@ -30,7 +30,7 @@ function reducer(state, action) { } } -const useRoadItemGetItems = (id) => { +const useRoadItemGetSubItems = (id) => { const requestServer = useRequest({ notificationShow: false }); const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { @@ -59,4 +59,4 @@ const useRoadItemGetItems = (id) => { }; }; -export default useRoadItemGetItems; +export default useRoadItemGetSubItems;