build and format

This commit is contained in:
2025-12-17 14:53:19 +03:30
parent 633da895c2
commit f78ee620cd
244 changed files with 16542 additions and 16551 deletions

View File

@@ -1,80 +1,80 @@
import LtrTextField from "@/core/components/LtrTextField";
import { Add, Close } from "@mui/icons-material";
import { Button, Collapse, Divider, IconButton, Stack, Typography } from "@mui/material";
import { TransitionGroup } from "react-transition-group";
const TollHouseArea = ({ value, onChange, error = [] }) => {
const handleChange = (index, field, newValue) => {
const newPoints = [...value];
newPoints[index] = { ...newPoints[index], [field]: newValue };
onChange(newPoints);
};
const handleAdd = () => {
onChange([...value, { lat: "", lon: "" }]);
};
const handleRemove = (index) => {
if (value.length > 4) {
const newPoints = value.filter((_, i) => i !== index);
onChange(newPoints);
}
};
return (
<TransitionGroup component={Stack} sx={{ my: 2 }}>
{value.map((point, index) => (
<Collapse key={index}>
<Stack direction={"row"} sx={{ my: 2 }} alignItems={"center"} spacing={1}>
<Typography>#{index + 1}</Typography>
{index >= 4 && (
<IconButton color="error" onClick={() => handleRemove(index)}>
<Close />
</IconButton>
)}
<Divider sx={{ flex: 1 }} />
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<LtrTextField
autoComplete="off"
size="small"
label="عرض جغرافیایی (Latitude)"
sx={{ width: 200 }}
InputLabelProps={{ shrink: true }}
value={point.lat}
onChange={(e) => handleChange(index, "lat", e.target.value)}
error={Boolean(error[index]?.lat)}
helperText={error[index]?.lat?.message}
/>
<LtrTextField
autoComplete="off"
sx={{ width: 200 }}
size="small"
label="طول جغرافیایی (Longitude)"
InputLabelProps={{ shrink: true }}
value={point.lon}
onChange={(e) => handleChange(index, "lon", e.target.value)}
error={Boolean(error[index]?.lon)}
helperText={error[index]?.lon?.message}
/>
</Stack>
</Stack>
</Collapse>
))}
<Collapse>
<Stack direction={"row"} alignItems={"center"} sx={{ mt: 2 }}>
<Divider sx={{ flex: 1 }}>
<Button
variant="outlined"
startIcon={<Add />}
onClick={handleAdd}
sx={{ alignSelf: "flex-start" }}
>
افزودن نقطه
</Button>
</Divider>
</Stack>
</Collapse>
</TransitionGroup>
);
};
export default TollHouseArea;
import LtrTextField from "@/core/components/LtrTextField";
import { Add, Close } from "@mui/icons-material";
import { Button, Collapse, Divider, IconButton, Stack, Typography } from "@mui/material";
import { TransitionGroup } from "react-transition-group";
const TollHouseArea = ({ value, onChange, error = [] }) => {
const handleChange = (index, field, newValue) => {
const newPoints = [...value];
newPoints[index] = { ...newPoints[index], [field]: newValue };
onChange(newPoints);
};
const handleAdd = () => {
onChange([...value, { lat: "", lon: "" }]);
};
const handleRemove = (index) => {
if (value.length > 4) {
const newPoints = value.filter((_, i) => i !== index);
onChange(newPoints);
}
};
return (
<TransitionGroup component={Stack} sx={{ my: 2 }}>
{value.map((point, index) => (
<Collapse key={index}>
<Stack direction={"row"} sx={{ my: 2 }} alignItems={"center"} spacing={1}>
<Typography>#{index + 1}</Typography>
{index >= 4 && (
<IconButton color="error" onClick={() => handleRemove(index)}>
<Close />
</IconButton>
)}
<Divider sx={{ flex: 1 }} />
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<LtrTextField
autoComplete="off"
size="small"
label="عرض جغرافیایی (Latitude)"
sx={{ width: 200 }}
InputLabelProps={{ shrink: true }}
value={point.lat}
onChange={(e) => handleChange(index, "lat", e.target.value)}
error={Boolean(error[index]?.lat)}
helperText={error[index]?.lat?.message}
/>
<LtrTextField
autoComplete="off"
sx={{ width: 200 }}
size="small"
label="طول جغرافیایی (Longitude)"
InputLabelProps={{ shrink: true }}
value={point.lon}
onChange={(e) => handleChange(index, "lon", e.target.value)}
error={Boolean(error[index]?.lon)}
helperText={error[index]?.lon?.message}
/>
</Stack>
</Stack>
</Collapse>
))}
<Collapse>
<Stack direction={"row"} alignItems={"center"} sx={{ mt: 2 }}>
<Divider sx={{ flex: 1 }}>
<Button
variant="outlined"
startIcon={<Add />}
onClick={handleAdd}
sx={{ alignSelf: "flex-start" }}
>
افزودن نقطه
</Button>
</Divider>
</Stack>
</Collapse>
</TransitionGroup>
);
};
export default TollHouseArea;

View File

@@ -1,29 +1,29 @@
import { useEffect, useRef } from "react";
import { FeatureGroup, useMap } from "react-leaflet";
const ShowBound = ({ bound }) => {
const map = useMap();
const featureRef = useRef(null);
const safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 20],
paddingBottomRight: [20, 20],
});
} catch (e) {
console.warn("fitBounds failed:", e);
}
};
useEffect(() => {
bound?.editing?.disable();
featureRef.current.addLayer(bound);
safeFitBounds(bound);
}, []);
return <FeatureGroup ref={featureRef} />;
};
export default ShowBound;
import { useEffect, useRef } from "react";
import { FeatureGroup, useMap } from "react-leaflet";
const ShowBound = ({ bound }) => {
const map = useMap();
const featureRef = useRef(null);
const safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 20],
paddingBottomRight: [20, 20],
});
} catch (e) {
console.warn("fitBounds failed:", e);
}
};
useEffect(() => {
bound?.editing?.disable();
featureRef.current.addLayer(bound);
safeFitBounds(bound);
}, []);
return <FeatureGroup ref={featureRef} />;
};
export default ShowBound;

View File

@@ -1,65 +1,65 @@
const { IconButton, Tooltip, Box, Dialog, DialogTitle, Typography, DialogContent } = require("@mui/material");
import MapLayer from "@/core/components/MapLayer";
import CloseIcon from "@mui/icons-material/Close";
import LayersIcon from "@mui/icons-material/Layers";
import L from "leaflet";
import { useMemo, useState } from "react";
import ShowBound from "./ShowBound";
const ShowProjectArea = ({ primaryArea }) => {
const [openShowAreaDialog, setOpenShowAreaDialog] = useState(false);
const bound = useMemo(() => {
const coordinates = Object.values(primaryArea.coordinates);
const latLngCoords = coordinates.map(([lat, lng]) => [parseFloat(lng), parseFloat(lat)]);
return primaryArea.type === "polygon"
? L.polygon(latLngCoords, { color: "#ff5555" })
: L.polyline(latLngCoords);
}, [primaryArea]);
return (
<Box>
<Tooltip title="نمایش پلیگان" placement="right" arrow>
<IconButton size="small" color="primary" onClick={() => setOpenShowAreaDialog(true)}>
<LayersIcon />
</IconButton>
</Tooltip>
<Dialog
open={openShowAreaDialog}
onClose={() => setOpenShowAreaDialog(false)}
PaperProps={{
sx: {
transition: "all .3s",
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
borderRadius: 2,
padding: 1,
},
}}
maxWidth="sm"
fullWidth
dir="rtl"
>
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
محدوده طرح
</Typography>
<IconButton onClick={() => setOpenShowAreaDialog(false)} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ flex: 1 }}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
</Box>
</Box>
</DialogContent>
</Dialog>
</Box>
);
};
export default ShowProjectArea;
const { IconButton, Tooltip, Box, Dialog, DialogTitle, Typography, DialogContent } = require("@mui/material");
import MapLayer from "@/core/components/MapLayer";
import CloseIcon from "@mui/icons-material/Close";
import LayersIcon from "@mui/icons-material/Layers";
import L from "leaflet";
import { useMemo, useState } from "react";
import ShowBound from "./ShowBound";
const ShowProjectArea = ({ primaryArea }) => {
const [openShowAreaDialog, setOpenShowAreaDialog] = useState(false);
const bound = useMemo(() => {
const coordinates = Object.values(primaryArea.coordinates);
const latLngCoords = coordinates.map(([lat, lng]) => [parseFloat(lng), parseFloat(lat)]);
return primaryArea.type === "polygon"
? L.polygon(latLngCoords, { color: "#ff5555" })
: L.polyline(latLngCoords);
}, [primaryArea]);
return (
<Box>
<Tooltip title="نمایش پلیگان" placement="right" arrow>
<IconButton size="small" color="primary" onClick={() => setOpenShowAreaDialog(true)}>
<LayersIcon />
</IconButton>
</Tooltip>
<Dialog
open={openShowAreaDialog}
onClose={() => setOpenShowAreaDialog(false)}
PaperProps={{
sx: {
transition: "all .3s",
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
borderRadius: 2,
padding: 1,
},
}}
maxWidth="sm"
fullWidth
dir="rtl"
>
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
محدوده طرح
</Typography>
<IconButton onClick={() => setOpenShowAreaDialog(false)} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ flex: 1 }}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
</Box>
</Box>
</DialogContent>
</Dialog>
</Box>
);
};
export default ShowProjectArea;

View File

@@ -1,25 +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;
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;

View File

@@ -1,44 +1,44 @@
"use client";
import { Box, Button, Drawer } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList";
import { useCallback, useState } from "react";
import FilterDrawer from "@/core/components/FilterDrawer";
const drawerSx = {
overflowY: "hidden",
display: "flex",
flexDirection: "column",
height: "100%",
zIndex: "1300",
};
const boxSx = {
width: { xs: 300, sm: 450 },
};
const Filter = ({ filterData, setFilterData }) => {
const [open, setOpen] = useState(false);
const openDrawer = useCallback(() => setOpen(true), []);
const closeDrawer = useCallback(() => setOpen(false), []);
return (
<>
<Button onClick={openDrawer} variant="outlined" size="small" endIcon={<FilterListIcon />}>
فیلتر
</Button>
<Drawer open={open} onClose={closeDrawer} sx={drawerSx}>
<Box sx={boxSx}>
{open && (
<FilterDrawer
defaultValues={filterData}
setFilterData={setFilterData}
closeDrawer={closeDrawer}
/>
)}
</Box>
</Drawer>
</>
);
};
export default Filter;
"use client";
import { Box, Button, Drawer } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList";
import { useCallback, useState } from "react";
import FilterDrawer from "@/core/components/FilterDrawer";
const drawerSx = {
overflowY: "hidden",
display: "flex",
flexDirection: "column",
height: "100%",
zIndex: "1300",
};
const boxSx = {
width: { xs: 300, sm: 450 },
};
const Filter = ({ filterData, setFilterData }) => {
const [open, setOpen] = useState(false);
const openDrawer = useCallback(() => setOpen(true), []);
const closeDrawer = useCallback(() => setOpen(false), []);
return (
<>
<Button onClick={openDrawer} variant="outlined" size="small" endIcon={<FilterListIcon />}>
فیلتر
</Button>
<Drawer open={open} onClose={closeDrawer} sx={drawerSx}>
<Box sx={boxSx}>
{open && (
<FilterDrawer
defaultValues={filterData}
setFilterData={setFilterData}
closeDrawer={closeDrawer}
/>
)}
</Box>
</Drawer>
</>
);
};
export default Filter;

View File

@@ -1,35 +1,35 @@
import { Box, Stack, Typography } from "@mui/material";
const Legend = () => {
return (
<Stack>
<Stack direction={"row"} spacing={1} alignItems={"center"}>
<Box
sx={{
width: 8,
height: 8,
border: 1,
borderRadius: "50%",
borderColor: (theme) => theme.palette.success.main,
background: (theme) => theme.palette.success.light,
}}
/>
<Typography variant="caption">فعال</Typography>
</Stack>
<Stack direction={"row"} spacing={1} alignItems={"center"}>
<Box
sx={{
width: 8,
height: 8,
border: 1,
borderRadius: "50%",
borderColor: (theme) => theme.palette.error.main,
background: (theme) => theme.palette.error.light,
}}
/>
<Typography variant="caption">غیرفعال</Typography>
</Stack>
</Stack>
);
};
export default Legend;
import { Box, Stack, Typography } from "@mui/material";
const Legend = () => {
return (
<Stack>
<Stack direction={"row"} spacing={1} alignItems={"center"}>
<Box
sx={{
width: 8,
height: 8,
border: 1,
borderRadius: "50%",
borderColor: (theme) => theme.palette.success.main,
background: (theme) => theme.palette.success.light,
}}
/>
<Typography variant="caption">فعال</Typography>
</Stack>
<Stack direction={"row"} spacing={1} alignItems={"center"}>
<Box
sx={{
width: 8,
height: 8,
border: 1,
borderRadius: "50%",
borderColor: (theme) => theme.palette.error.main,
background: (theme) => theme.palette.error.light,
}}
/>
<Typography variant="caption">غیرفعال</Typography>
</Stack>
</Stack>
);
};
export default Legend;

View File

@@ -1,71 +1,71 @@
import { Box, Chip, Divider, Stack, Typography } from "@mui/material";
import moment from "jalali-moment";
import Image from "next/image";
const ContentInfoItem = ({ data }) => {
return (
<Stack spacing={2}>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="کد یکتا" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>{data.id !== "" && data.id}</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="استان" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.province_name !== "" && data.province_name}
</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="نوع" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.type_fa !== "" && data.type_fa}
</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="وضعیت" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.status === "1" ? "فعال" : "غیرفعال"}
</Typography>
</Stack>
<Stack spacing={2}>
<Stack
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
spacing={1}
alignItems={"center"}
>
<Chip size="small" label="تصویر بازدید" />
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
<Image
src={data.files[0].path ?? ""}
style={{ objectFit: "contain" }}
alt="recognize_picture"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</Box>
</Stack>
<Stack
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
spacing={1}
alignItems={"center"}
>
<Chip size="small" label="تصویر بازدید" />
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
<Image
src={data.files[1].path ?? ""}
style={{ objectFit: "contain" }}
alt="recognize_picture_second"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</Box>
</Stack>
</Stack>
</Stack>
);
};
export default ContentInfoItem;
import { Box, Chip, Divider, Stack, Typography } from "@mui/material";
import moment from "jalali-moment";
import Image from "next/image";
const ContentInfoItem = ({ data }) => {
return (
<Stack spacing={2}>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="کد یکتا" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>{data.id !== "" && data.id}</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="استان" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.province_name !== "" && data.province_name}
</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="نوع" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.type_fa !== "" && data.type_fa}
</Typography>
</Stack>
<Stack direction={"row"} alignItems={"center"}>
<Chip size="small" label="وضعیت" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{data.status === "1" ? "فعال" : "غیرفعال"}
</Typography>
</Stack>
<Stack spacing={2}>
<Stack
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
spacing={1}
alignItems={"center"}
>
<Chip size="small" label="تصویر بازدید" />
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
<Image
src={data.files[0].path ?? ""}
style={{ objectFit: "contain" }}
alt="recognize_picture"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</Box>
</Stack>
<Stack
sx={{ flex: 1, border: 1, borderColor: "divider", borderRadius: 2, p: 1 }}
spacing={1}
alignItems={"center"}
>
<Chip size="small" label="تصویر بازدید" />
<Box sx={{ position: "relative", height: 300, width: "100%" }}>
<Image
src={data.files[1].path ?? ""}
style={{ objectFit: "contain" }}
alt="recognize_picture_second"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</Box>
</Stack>
</Stack>
</Stack>
);
};
export default ContentInfoItem;

View File

@@ -1,51 +1,51 @@
import DialogLoading from "@/core/components/DialogLoading";
import { GET_TOLL_HOUSE_DETAILS } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
import { useEffect, useState } from "react";
import ContentInfoItem from "./ContentInfoItem";
const DialogInfoItem = ({ selectedId, closeDialog }) => {
const request = useRequest();
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchItem = async () => {
setLoading(true);
setData(null);
try {
const response = await request(`${GET_TOLL_HOUSE_DETAILS}/${selectedId}`, "get", {
signal: controller.signal,
});
setData(response.data.data);
} catch (error) {
} finally {
setLoading(false);
}
};
fetchItem();
return () => {
controller.abort();
};
}, [selectedId]);
return (
<>
<DialogTitle>جزئیات فعالیت</DialogTitle>
<DialogContent dividers>
{loading ? <DialogLoading /> : data && <ContentInfoItem data={data} />}
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} variant="outlined" color="secondary" autoFocus>
بستن
</Button>
</DialogActions>
</>
);
};
export default DialogInfoItem;
import DialogLoading from "@/core/components/DialogLoading";
import { GET_TOLL_HOUSE_DETAILS } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
import { useEffect, useState } from "react";
import ContentInfoItem from "./ContentInfoItem";
const DialogInfoItem = ({ selectedId, closeDialog }) => {
const request = useRequest();
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchItem = async () => {
setLoading(true);
setData(null);
try {
const response = await request(`${GET_TOLL_HOUSE_DETAILS}/${selectedId}`, "get", {
signal: controller.signal,
});
setData(response.data.data);
} catch (error) {
} finally {
setLoading(false);
}
};
fetchItem();
return () => {
controller.abort();
};
}, [selectedId]);
return (
<>
<DialogTitle>جزئیات فعالیت</DialogTitle>
<DialogContent dividers>
{loading ? <DialogLoading /> : data && <ContentInfoItem data={data} />}
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} variant="outlined" color="secondary" autoFocus>
بستن
</Button>
</DialogActions>
</>
);
};
export default DialogInfoItem;

View File

@@ -1,97 +1,97 @@
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 }) => {
if (!lat && !lng) return;
return (
<CircleMarker
key={id}
center={L.latLng(Number(lat), Number(lng))}
radius={8}
eventHandlers={{
click: () => {
openDialog();
setSelectedId(id);
},
}}
color={getMarkerColor(Number(status))}
/>
);
});
}, [data, theme]);
useEffect(() => {
if (data.length === 0) {
// fly to Iran
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
// ساختن bounds با حذف مواردی که lat/lng ندارند
const validPoints = data
.filter(({ lat, lng }) => lat && lng)
.map(({ lat, lng }) => L.latLng(Number(lat), Number(lng)));
if (validPoints.length === 0) {
// اگر هیچ مختصات معتبری نبود → ایران
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
const bounds = L.latLngBounds(validPoints);
if (!bounds.isValid()) {
// اگر bounds معتبر نبود → fallback روی ایران
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
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;
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 }) => {
if (!lat && !lng) return;
return (
<CircleMarker
key={id}
center={L.latLng(Number(lat), Number(lng))}
radius={8}
eventHandlers={{
click: () => {
openDialog();
setSelectedId(id);
},
}}
color={getMarkerColor(Number(status))}
/>
);
});
}, [data, theme]);
useEffect(() => {
if (data.length === 0) {
// fly to Iran
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
// ساختن bounds با حذف مواردی که lat/lng ندارند
const validPoints = data
.filter(({ lat, lng }) => lat && lng)
.map(({ lat, lng }) => L.latLng(Number(lat), Number(lng)));
if (validPoints.length === 0) {
// اگر هیچ مختصات معتبری نبود → ایران
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
const bounds = L.latLngBounds(validPoints);
if (!bounds.isValid()) {
// اگر bounds معتبر نبود → fallback روی ایران
map.flyTo([32.4279, 53.688], 6, { duration: 0.7 });
return;
}
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;

View File

@@ -1,231 +1,231 @@
"use client";
import LoadingHardPage from "@/core/components/LoadingHardPage";
import MapLoading from "@/core/components/MapLayer/Loading";
import isArrayEmpty from "@/core/utils/isArrayEmpty";
import { GET_TOLL_HOUSE_MAP } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Box, Stack } from "@mui/material";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useState } from "react";
import ClusterSwitch from "./ClusterSwitch";
import Filter from "./Filter";
import Legend from "./Legend";
import PointsOnMap from "./PointsOnMap";
import DataTableFilterDataStructure from "@/core/utils/DataTableFilterDataStructure";
import useProvinces from "@/lib/hooks/useProvince";
import CustomSelectByDependency from "@/core/components/FilterDrawer/fieldsType/CustomSelectByDependency";
import useEdaratLists from "@/lib/hooks/useEdaratLists";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const typeOptions = [
{ value: "", label: "همه نوع ها" },
{ value: 0, label: "فصلی" },
{ value: 1, label: "دائمی" },
{ value: 2, label: "موقت" },
];
const MAX_POINTS = 1000;
const containerStyles = {
width: "100%",
height: "100%",
p: 1,
border: "1px dashed",
borderColor: "divider",
borderRadius: 1,
};
const controlBarStyles = {
p: 1,
background: "#fff",
};
const RoadSafetyReportMap = () => {
const defaultValues = useMemo(
() => [
{
header: "استان",
id: "province_id",
filterMode: "equals",
datatype: "numeric",
value: "",
enable: true,
SelectComponent: (props) => {
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const getSelectOptions = useMemo(() => {
if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل کشور" },
...provinces.map((province) => ({
value: province.id,
label: province.name_fa,
})),
];
}, [provinces, errorProvinces, loadingProvinces]);
return (
<CustomSelectByDependency
{...props}
value={loadingProvinces ? "loading" : props.filterParameters.value}
selectOption={getSelectOptions}
/>
);
},
},
{
eader: "اداره",
id: "edare_shahri_id",
filterMode: "equals",
datatype: "numeric",
dependencyId: "province_id",
value: "",
enable: true,
SelectComponent: (props) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
props.dependencyFieldValue.value
);
const [prevDependency, setPrevDependency] = useState(props.dependencyFieldValue.value);
const getSelectOptions = useMemo(() => {
if (props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingEdaratList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorEdaratList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...edaratList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [edaratList, loadingEdaratList, errorEdaratList]);
useEffect(() => {
if (prevDependency === props.dependencyFieldValue?.value) return;
props.handleChange({ ...props.filterParameters, value: "" });
setPrevDependency(props.dependencyFieldValue?.value);
}, [props.dependencyFieldValue?.value]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingEdaratList
? "loading"
: props.filterParameters.value
}
selectOption={getSelectOptions}
/>
);
},
},
{
header: "نوع",
id: "type",
filterMode: "equals",
datatype: "numeric",
value: "",
enable: true,
selectOption: () => {
return typeOptions.map((status) => ({
value: status.value,
label: status.label,
}));
},
},
],
[]
);
const requestServer = useRequest();
const [filterData, setFilterData] = useState(
defaultValues.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {})
);
const [isCluster, setCluster] = useState(false);
const [data, setData] = useState([]);
const [isLoading, setLoading] = useState(true);
const buildQueryParams = useCallback(() => {
const params = new URLSearchParams();
const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
params.set("filters", JSON.stringify(filters || []));
return params.toString();
}, [filterData]);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
params.set("filters", JSON.stringify(filters || []));
const query = params.toString();
const response = await requestServer(`${GET_TOLL_HOUSE_MAP}?${query}`);
const fetchedData = response?.data?.data || [];
if (fetchedData.length > MAX_POINTS) {
setCluster(true);
}
setData(fetchedData);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [filterData, buildQueryParams]);
return (
<Box sx={containerStyles}>
<MapLayer style={{ borderRadius: "4px" }}>
<Stack
direction="row-reverse"
sx={{ position: "absolute", top: 0, right: 0, width: "100%", zIndex: 1000 }}
>
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, borderBottomLeftRadius: "4px" }}>
<ClusterSwitch
isCluster={isCluster}
setCluster={setCluster}
disabled={data.length > MAX_POINTS}
max={MAX_POINTS}
/>
<Filter filterData={filterData} setFilterData={setFilterData} />
</Stack>
</Stack>
<Stack
direction="row-reverse"
sx={{ position: "absolute", bottom: 0, right: 0, width: "100%", zIndex: 1000 }}
>
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, pb: 0, borderTopLeftRadius: "4px" }}>
<Legend />
</Stack>
</Stack>
{isLoading && (
<Box sx={{ position: "relative", width: "100%", height: "100%", opacity: 0.7, zIndex: 999 }}>
<LoadingHardPage width={80} height={80} loading={isLoading} sx={{ position: "absolute" }} />
</Box>
)}
<PointsOnMap data={data} isCluster={isCluster} />
</MapLayer>
</Box>
);
};
export default RoadSafetyReportMap;
"use client";
import LoadingHardPage from "@/core/components/LoadingHardPage";
import MapLoading from "@/core/components/MapLayer/Loading";
import isArrayEmpty from "@/core/utils/isArrayEmpty";
import { GET_TOLL_HOUSE_MAP } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Box, Stack } from "@mui/material";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useState } from "react";
import ClusterSwitch from "./ClusterSwitch";
import Filter from "./Filter";
import Legend from "./Legend";
import PointsOnMap from "./PointsOnMap";
import DataTableFilterDataStructure from "@/core/utils/DataTableFilterDataStructure";
import useProvinces from "@/lib/hooks/useProvince";
import CustomSelectByDependency from "@/core/components/FilterDrawer/fieldsType/CustomSelectByDependency";
import useEdaratLists from "@/lib/hooks/useEdaratLists";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const typeOptions = [
{ value: "", label: "همه نوع ها" },
{ value: 0, label: "فصلی" },
{ value: 1, label: "دائمی" },
{ value: 2, label: "موقت" },
];
const MAX_POINTS = 1000;
const containerStyles = {
width: "100%",
height: "100%",
p: 1,
border: "1px dashed",
borderColor: "divider",
borderRadius: 1,
};
const controlBarStyles = {
p: 1,
background: "#fff",
};
const RoadSafetyReportMap = () => {
const defaultValues = useMemo(
() => [
{
header: "استان",
id: "province_id",
filterMode: "equals",
datatype: "numeric",
value: "",
enable: true,
SelectComponent: (props) => {
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const getSelectOptions = useMemo(() => {
if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل کشور" },
...provinces.map((province) => ({
value: province.id,
label: province.name_fa,
})),
];
}, [provinces, errorProvinces, loadingProvinces]);
return (
<CustomSelectByDependency
{...props}
value={loadingProvinces ? "loading" : props.filterParameters.value}
selectOption={getSelectOptions}
/>
);
},
},
{
eader: "اداره",
id: "edare_shahri_id",
filterMode: "equals",
datatype: "numeric",
dependencyId: "province_id",
value: "",
enable: true,
SelectComponent: (props) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
props.dependencyFieldValue.value
);
const [prevDependency, setPrevDependency] = useState(props.dependencyFieldValue.value);
const getSelectOptions = useMemo(() => {
if (props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingEdaratList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorEdaratList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...edaratList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [edaratList, loadingEdaratList, errorEdaratList]);
useEffect(() => {
if (prevDependency === props.dependencyFieldValue?.value) return;
props.handleChange({ ...props.filterParameters, value: "" });
setPrevDependency(props.dependencyFieldValue?.value);
}, [props.dependencyFieldValue?.value]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingEdaratList
? "loading"
: props.filterParameters.value
}
selectOption={getSelectOptions}
/>
);
},
},
{
header: "نوع",
id: "type",
filterMode: "equals",
datatype: "numeric",
value: "",
enable: true,
selectOption: () => {
return typeOptions.map((status) => ({
value: status.value,
label: status.label,
}));
},
},
],
[]
);
const requestServer = useRequest();
const [filterData, setFilterData] = useState(
defaultValues.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {})
);
const [isCluster, setCluster] = useState(false);
const [data, setData] = useState([]);
const [isLoading, setLoading] = useState(true);
const buildQueryParams = useCallback(() => {
const params = new URLSearchParams();
const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
params.set("filters", JSON.stringify(filters || []));
return params.toString();
}, [filterData]);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
const filters = DataTableFilterDataStructure(filterData, isArrayEmpty);
params.set("filters", JSON.stringify(filters || []));
const query = params.toString();
const response = await requestServer(`${GET_TOLL_HOUSE_MAP}?${query}`);
const fetchedData = response?.data?.data || [];
if (fetchedData.length > MAX_POINTS) {
setCluster(true);
}
setData(fetchedData);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [filterData, buildQueryParams]);
return (
<Box sx={containerStyles}>
<MapLayer style={{ borderRadius: "4px" }}>
<Stack
direction="row-reverse"
sx={{ position: "absolute", top: 0, right: 0, width: "100%", zIndex: 1000 }}
>
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, borderBottomLeftRadius: "4px" }}>
<ClusterSwitch
isCluster={isCluster}
setCluster={setCluster}
disabled={data.length > MAX_POINTS}
max={MAX_POINTS}
/>
<Filter filterData={filterData} setFilterData={setFilterData} />
</Stack>
</Stack>
<Stack
direction="row-reverse"
sx={{ position: "absolute", bottom: 0, right: 0, width: "100%", zIndex: 1000 }}
>
<Stack direction="row" spacing={1} sx={{ ...controlBarStyles, pb: 0, borderTopLeftRadius: "4px" }}>
<Legend />
</Stack>
</Stack>
{isLoading && (
<Box sx={{ position: "relative", width: "100%", height: "100%", opacity: 0.7, zIndex: 999 }}>
<LoadingHardPage width={80} height={80} loading={isLoading} sx={{ position: "absolute" }} />
</Box>
)}
<PointsOnMap data={data} isCluster={isCluster} />
</MapLayer>
</Box>
);
};
export default RoadSafetyReportMap;