Merge branch 'feature/amiriis_parakandegi' into 'develop'
Feature/amiriis parakandegi See merge request witel-front-end/rms!62
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-leaflet-draw": "^0.20.4",
|
"react-leaflet-draw": "^0.20.4",
|
||||||
|
"react-leaflet-markercluster": "^4.2.1",
|
||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.75.0",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import RoadItemsReportMap from "@/components/dashboard/roadItems/reports/Map";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <RoadItemsReportMap />;
|
||||||
|
};
|
||||||
|
export default Page;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "fontiran";
|
@import "fontiran";
|
||||||
@import "leaflet/dist/leaflet.css";
|
@import "leaflet/dist/leaflet.css";
|
||||||
@import "react-toastify/dist/ReactToastify.css";
|
@import "react-toastify/dist/ReactToastify.css";
|
||||||
|
@import "./map-styles.scss";
|
||||||
|
|
||||||
.filter-toast {
|
.filter-toast {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
|||||||
@@ -1,27 +1,9 @@
|
|||||||
.leaflet-draw-tooltip {
|
.custom-marker-cluster {
|
||||||
background-color: #4f4f4f;
|
font-family: IRANSansFaNum;
|
||||||
font-family: IRANSans;
|
background-color: rgba(51, 181, 229, 0.6);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
border-radius: 50%;
|
||||||
padding: 2px 8px;
|
text-align: center;
|
||||||
margin-right: 15px;
|
font-size: 14px;
|
||||||
border-radius: 4px;
|
line-height: 40px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import "leaflet-draw/dist/leaflet.draw.css";
|
|
||||||
import "@/assets/scss/map-styles.scss";
|
|
||||||
import { FeatureGroup, useMap } from "react-leaflet";
|
import { FeatureGroup, useMap } from "react-leaflet";
|
||||||
import { EditControl } from "react-leaflet-draw";
|
import { EditControl } from "react-leaflet-draw";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ const OperatorList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: "وضعیت", // Status
|
header: "وضعیت نظارت",
|
||||||
id: "status",
|
id: "status",
|
||||||
enableColumnFilter: true,
|
enableColumnFilter: true,
|
||||||
datatype: "numeric",
|
datatype: "numeric",
|
||||||
@@ -318,7 +318,7 @@ const OperatorList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "supervisor_description",
|
accessorKey: "supervisor_description",
|
||||||
header: "توضیحات کارشناس", // Description
|
header: "توضیحات ناظر",
|
||||||
id: "supervisor_description",
|
id: "supervisor_description",
|
||||||
enableColumnFilter: false,
|
enableColumnFilter: false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const DescriptionForm = ({ description }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="توضیحات کارشناس">
|
<Tooltip title="توضیحات ناظر">
|
||||||
<IconButton color="primary" onClick={() => setOpenOfficerDescriptionDialog(true)}>
|
<IconButton color="primary" onClick={() => setOpenOfficerDescriptionDialog(true)}>
|
||||||
<RemoveRedEyeIcon />
|
<RemoveRedEyeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -30,7 +30,7 @@ const DescriptionForm = ({ description }) => {
|
|||||||
>
|
>
|
||||||
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
|
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
|
||||||
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
|
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
|
||||||
توضیحات کارشناس
|
توضیحات ناظر
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={() => setOpenOfficerDescriptionDialog(false)} size="small">
|
<IconButton onClick={() => setOpenOfficerDescriptionDialog(false)} size="small">
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 (
|
||||||
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
|
<MuiDatePicker
|
||||||
|
label={"تاریخ فعالیت"}
|
||||||
|
name={`activity_date_time[0]`}
|
||||||
|
value={value[0]}
|
||||||
|
setFieldValue={(name, formattedDate) => {
|
||||||
|
onChange([formattedDate, value[1]]);
|
||||||
|
}}
|
||||||
|
maxDate={value[1]}
|
||||||
|
placeholder={`از تاریخ`}
|
||||||
|
/>
|
||||||
|
<MuiDatePicker
|
||||||
|
label={"تاریخ فعالیت"}
|
||||||
|
name={`activity_date_time[1]`}
|
||||||
|
value={value[1]}
|
||||||
|
setFieldValue={(name, formattedDate) => {
|
||||||
|
onChange([value[0], formattedDate]);
|
||||||
|
}}
|
||||||
|
minDate={value[0]}
|
||||||
|
placeholder={`تا تاریخ`}
|
||||||
|
helperText={error ? error.message : null}
|
||||||
|
error={Boolean(error)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ActivityDateTimeFilter;
|
||||||
@@ -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 <EdarehShahriController value={value} onChange={onChange} dependencyField={dependencyField} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
|
||||||
|
<InputLabel id={`labeledarat_id`} shrink>
|
||||||
|
اداره
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`labeledarat_id`}
|
||||||
|
id={"edarat_id"}
|
||||||
|
name={`edarat_id`}
|
||||||
|
value={dependencyField === "" ? "empty" : loadingEdaratList ? "loading" : value}
|
||||||
|
input={<OutlinedInput label="اداره" />}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{columnSelectOption.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EdarehShahriFilter;
|
||||||
@@ -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 (
|
||||||
|
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
|
||||||
|
<InputLabel id={`labelitem_id`} shrink>
|
||||||
|
آیتم فعالیت
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`labelitem_id`}
|
||||||
|
id={"item_id"}
|
||||||
|
name={`item_id`}
|
||||||
|
value={loadingItemsList ? "loading" : value}
|
||||||
|
input={<OutlinedInput label="آیتم فعالیت" />}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{columnSelectOption.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ItemFilter;
|
||||||
@@ -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 (
|
||||||
|
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
|
||||||
|
<InputLabel id={`labelprovince_id`} shrink>
|
||||||
|
استان
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`labelprovince_id`}
|
||||||
|
id={"province_id"}
|
||||||
|
name={`province_id`}
|
||||||
|
value={loadingProvinces ? "loading" : value}
|
||||||
|
input={<OutlinedInput label="استان" />}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{columnSelectOption.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ProvinceFilter;
|
||||||
@@ -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 (
|
||||||
|
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
|
||||||
|
<InputLabel id={`labelstatus`} shrink>
|
||||||
|
وضعیت نظارت
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`labelstatus`}
|
||||||
|
id={"status"}
|
||||||
|
name={`status`}
|
||||||
|
value={value}
|
||||||
|
input={<OutlinedInput label="وضعیت نظارت" />}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{statusOptions.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default StatusFilter;
|
||||||
@@ -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 <SubItemController value={value} onChange={onChange} dependencyField={dependencyField} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
|
||||||
|
<InputLabel id={`labelsub_item_id`} shrink>
|
||||||
|
اقدام انجام شده
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`labelsub_item_id`}
|
||||||
|
id={"sub_item_id"}
|
||||||
|
name={`sub_item_id`}
|
||||||
|
value={dependencyField === "" ? "empty" : loadingSubItemsList ? "loading" : value}
|
||||||
|
input={<OutlinedInput label="اقدام انجام شده" />}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{columnSelectOption.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SubItemFilter;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Box sx={headerSx}>
|
||||||
|
<Box sx={headerTitleSx}>
|
||||||
|
<FilterAltIcon sx={headerIconSx} />
|
||||||
|
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||||
|
فیلتر
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton sx={iconButtonSx} onClick={closeDrawer}>
|
||||||
|
<CancelIcon sx={{ fontSize: "1em" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ScrollBox>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Box sx={formContainerSx}>
|
||||||
|
<Controller
|
||||||
|
name="province_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<ProvinceFilter value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="edarat_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<EdarehShahriFilter value={value} onChange={onChange} control={control} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="item_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<ItemFilter value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="sub_item_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SubItemFilter value={value} onChange={onChange} control={control} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="activity_date_time"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<ActivityDateTimeFilter value={value} onChange={onChange} error={error} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="status"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<StatusFilter value={value} onChange={onChange} control={control} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={footerSx}>
|
||||||
|
<Button type="submit" variant="contained" size="large" disabled={!isDirty} sx={submitButtonSx}>
|
||||||
|
اعمال فیلتر
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</ScrollBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default FilterDrawer;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<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;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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.primary.main,
|
||||||
|
background: (theme) => theme.palette.primary.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.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.warning.main,
|
||||||
|
background: (theme) => theme.palette.warning.light,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">عدم تایید</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Legend;
|
||||||
@@ -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 (
|
||||||
|
<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_fa !== "" && data.province_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.edarat_name !== "" && data.edarat_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.item_fa !== "" && data.item_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.sub_item_fa !== "" && data.sub_item_fa}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"}>
|
||||||
|
<Chip size="small" label="مقدار" />
|
||||||
|
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||||
|
<Typography sx={{ fontSize: "13px", fontWeight: 400 }} component="span">
|
||||||
|
{(data.sub_item_data / 1).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
<Typography component="span" variant="caption">
|
||||||
|
{`(${data.unit_fa})`}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"}>
|
||||||
|
<Chip size="small" label="تاریخ فعالیت" />
|
||||||
|
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||||
|
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
|
||||||
|
{data.activity_date_time &&
|
||||||
|
moment(data.activity_date_time).locale("fa").format("HH:mm | yyyy/MM/DD")}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"}>
|
||||||
|
<Chip size="small" label="تاریخ ثبت" />
|
||||||
|
<Divider sx={{ flex: 1, mx: 2 }} />
|
||||||
|
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
|
||||||
|
{data.created_at && moment(data.created_at).locale("fa").format("HH:mm | yyyy/MM/DD")}
|
||||||
|
</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_fa !== "" && data.status_fa}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }} alignItems={"center"}>
|
||||||
|
<Chip size="small" label="قبل از اقدام" />
|
||||||
|
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
|
||||||
|
<Image
|
||||||
|
src={data.files[1].full_path_for_fast_react}
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
alt="before_image"
|
||||||
|
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 }} alignItems={"center"}>
|
||||||
|
<Chip size="small" label="بعد از اقدام" />
|
||||||
|
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
|
||||||
|
<Image
|
||||||
|
src={data.files[0].full_path_for_fast_react}
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
alt="before_image"
|
||||||
|
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_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 (
|
||||||
|
<>
|
||||||
|
<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,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 }) => (
|
||||||
|
<CircleMarker
|
||||||
|
eventHandlers={{
|
||||||
|
click: () => {
|
||||||
|
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: `<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;
|
||||||
127
src/components/dashboard/roadItems/reports/Map/index.jsx
Normal file
127
src/components/dashboard/roadItems/reports/Map/index.jsx
Normal file
@@ -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: () => <MapLoading />,
|
||||||
|
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 (
|
||||||
|
<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 RoadItemsReportMap;
|
||||||
@@ -404,7 +404,7 @@ const SupervisorList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: "وضعیت", // Status
|
header: "وضعیت نظارت",
|
||||||
id: "status",
|
id: "status",
|
||||||
enableColumnFilter: true,
|
enableColumnFilter: true,
|
||||||
datatype: "numeric",
|
datatype: "numeric",
|
||||||
|
|||||||
@@ -1,32 +1,15 @@
|
|||||||
"use client";
|
"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 FilterHeader from "@/core/components/DataTable/filter/FilterHeader";
|
||||||
import useDataTable from "@/lib/hooks/useDataTable";
|
import useDataTable from "@/lib/hooks/useDataTable";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
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 { useEffect, useState } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
import ScrollBox from "../../ScrollBox";
|
||||||
import FilterBodyController from "./FilterBodyController";
|
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 }) {
|
function FilterBody({ columns, drawerState, setDrawerState }) {
|
||||||
const { filterData, setFilterData } = useDataTable();
|
const { filterData, setFilterData } = useDataTable();
|
||||||
@@ -89,7 +72,7 @@ function FilterBody({ columns, drawerState, setDrawerState }) {
|
|||||||
{Object.keys(filterData).length > 0 && (
|
{Object.keys(filterData).length > 0 && (
|
||||||
<ScrollBox>
|
<ScrollBox>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Box sx={{ px: 1, py: 3 }}>
|
<Box sx={{ px: 2, py: 3 }}>
|
||||||
{columns.map((column) =>
|
{columns.map((column) =>
|
||||||
column.enableColumnFilter ? (
|
column.enableColumnFilter ? (
|
||||||
column.dependencyId ? (
|
column.dependencyId ? (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import FilterButton from "@/core/components/DataTable/filter/FilterButton";
|
import FilterButton from "@/core/components/DataTable/filter/FilterButton";
|
||||||
import FilterBody from "@/core/components/DataTable/filter/FilterBody";
|
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 }) {
|
function FilterColumn({ columns, user_id, page_name, table_name }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -15,14 +15,18 @@ function FilterColumn({ columns, user_id, page_name, table_name }) {
|
|||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
sx={{ overflowY: "hidden", display: "flex", flexDirection: "column", height: "100%", zIndex: "1300" }}
|
sx={{ overflowY: "hidden", display: "flex", flexDirection: "column", height: "100%", zIndex: "1300" }}
|
||||||
>
|
>
|
||||||
<FilterBody
|
<Box sx={{ width: { xs: 300, sm: 450 } }}>
|
||||||
columns={columns}
|
{open && (
|
||||||
drawerState={open}
|
<FilterBody
|
||||||
setDrawerState={setOpen}
|
columns={columns}
|
||||||
user_id={user_id}
|
drawerState={open}
|
||||||
page_name={page_name}
|
setDrawerState={setOpen}
|
||||||
table_name={table_name}
|
user_id={user_id}
|
||||||
/>
|
page_name={page_name}
|
||||||
|
table_name={table_name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import SvgError from "@/core/components/svgs/SvgError";
|
|||||||
const LoadingImage = styled(Box)({
|
const LoadingImage = styled(Box)({
|
||||||
"@keyframes load": {
|
"@keyframes load": {
|
||||||
"0%": {
|
"0%": {
|
||||||
// opacity: 0,
|
|
||||||
transform: "scale(1)",
|
transform: "scale(1)",
|
||||||
},
|
},
|
||||||
"50%": {
|
"50%": {
|
||||||
// opacity: 1,
|
|
||||||
transform: "scale(.5)",
|
transform: "scale(.5)",
|
||||||
},
|
},
|
||||||
"100%": {
|
"100%": {
|
||||||
// opacity: 0,
|
|
||||||
transform: "scale(1)",
|
transform: "scale(1)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ const createCustomIcon = (size, iconUrl, labelText, color) => {
|
|||||||
const MAX_ZOOM_FOR_MARKER = 13;
|
const MAX_ZOOM_FOR_MARKER = 13;
|
||||||
|
|
||||||
const MapInteraction = ({ setValue, startLat, startLng }) => {
|
const MapInteraction = ({ setValue, startLat, startLng }) => {
|
||||||
const [isMarkerLocked, setIsMarkerLocked] = useState(!!(startLat && startLng)); // وضعیت قفل مارکر
|
const [isMarkerLocked, setIsMarkerLocked] = useState(!!(startLat && startLng));
|
||||||
const [enableSend, setEnableSend] = useState(false);
|
const [enableSend, setEnableSend] = useState(false);
|
||||||
const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); // رنگ آیکن شروع
|
const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f");
|
||||||
const [markerPosition, setMarkerPosition] = useState(
|
const [markerPosition, setMarkerPosition] = useState(
|
||||||
startLat && startLng ? { lat: startLat, lng: startLng } : null
|
startLat && startLng ? { lat: startLat, lng: startLng } : null
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ const createCustomIcon = (size, iconUrl, labelText, color) => {
|
|||||||
const MAX_ZOOM_FOR_MARKER = 13;
|
const MAX_ZOOM_FOR_MARKER = 13;
|
||||||
|
|
||||||
const MapInteraction = ({ setValue, startLat, startLng, endLat, endLng }) => {
|
const MapInteraction = ({ setValue, startLat, startLng, endLat, endLng }) => {
|
||||||
const [isStartLocked, setIsStartLocked] = useState(!!(startLat && startLng)); // وضعیت قفل نقطه آغاز
|
const [isStartLocked, setIsStartLocked] = useState(!!(startLat && startLng));
|
||||||
const [isEndLocked, setIsEndLocked] = useState(!!(endLat && endLng)); // وضعیت قفل نقطه پایان
|
const [isEndLocked, setIsEndLocked] = useState(!!(endLat && endLng));
|
||||||
const [startPosition, setStartPosition] = useState(startLat && startLng ? { lat: startLat, lng: startLng } : null);
|
const [startPosition, setStartPosition] = useState(startLat && startLng ? { lat: startLat, lng: startLng } : null);
|
||||||
const [endPosition, setEndPosition] = useState(endLat && endLng ? { lat: endLat, lng: endLng } : null);
|
const [endPosition, setEndPosition] = useState(endLat && endLng ? { lat: endLat, lng: endLng } : null);
|
||||||
const [enableSend, setEnableSend] = useState(false);
|
const [enableSend, setEnableSend] = useState(false);
|
||||||
const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f"); // رنگ آیکن شروع
|
const [startIconColor, setStartIconColor] = useState(startLat ? "#1CAC66" : "#003d4f");
|
||||||
const [endIconColor, setEndIconColor] = useState(endLat ? "#D13131" : "#003d4f"); // رنگ آیکن پایان
|
const [endIconColor, setEndIconColor] = useState(endLat ? "#D13131" : "#003d4f");
|
||||||
const startRef = useRef();
|
const startRef = useRef();
|
||||||
const endRef = useRef();
|
const endRef = useRef();
|
||||||
|
|
||||||
|
|||||||
19
src/core/components/ScrollBox.jsx
Normal file
19
src/core/components/ScrollBox.jsx
Normal file
@@ -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;
|
||||||
@@ -137,7 +137,7 @@ export const pageMenu = [
|
|||||||
id: "roadItemManagmentMap",
|
id: "roadItemManagmentMap",
|
||||||
label: "پراکندگی بر روی نقشه",
|
label: "پراکندگی بر روی نقشه",
|
||||||
type: "page",
|
type: "page",
|
||||||
route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_items",
|
route: "/dashboard/road-items/reports/map",
|
||||||
icon: <MapIcon sx={{ width: "inherit", height: "inherit" }} />,
|
icon: <MapIcon sx={{ width: "inherit", height: "inherit" }} />,
|
||||||
permissions: [
|
permissions: [
|
||||||
"show-road-item-supervise-cartable",
|
"show-road-item-supervise-cartable",
|
||||||
|
|||||||
@@ -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_CAR_LIST_SEARCH = api + "/api/v3/cmms_machines/search";
|
||||||
export const GET_RAHDARANS_LIST_SEARCH = api + "/api/v3/rahdaran/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_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_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_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";
|
export const GET_PROVINCE_ACTIVITY_PER_SUB_ITEM = api + "/api/v3/road_item_reports/country_activity_per_sub_item";
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function reducer(state, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRoadItemGetItems = (id) => {
|
const useRoadItemGetSubItems = (id) => {
|
||||||
const requestServer = useRequest({ notificationShow: false });
|
const requestServer = useRequest({ notificationShow: false });
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,4 +59,4 @@ const useRoadItemGetItems = (id) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useRoadItemGetItems;
|
export default useRoadItemGetSubItems;
|
||||||
|
|||||||
Reference in New Issue
Block a user