29 Commits

Author SHA1 Message Date
3308f14d49 Merge branch 'release/v1.9.1' 2026-06-22 15:21:29 +03:30
7f77165ec4 v1.9.1 2026-06-22 15:20:40 +03:30
0435654762 Merge pull request 'feature/moddle_point' (#12) from feature/moddle_point into develop
Reviewed-on: #12
2026-06-22 11:48:26 +00:00
f94374958d build and format 2026-06-22 15:15:04 +03:30
a26f07d2ad 2 middle point 2026-06-22 15:14:02 +03:30
d1b74585d7 build & format 2026-06-22 15:04:47 +03:30
6115e0ded2 refactored violations, add controller and edit
replaced text explanation with select fields
2026-06-22 14:56:02 +03:30
40f80fac3a refactored explanation field on create flow
road missions operator cartable
2026-06-22 12:03:39 +03:30
188c7c45af base select options 2026-06-21 15:03:20 +03:30
f6c369f866 Merge pull request 'feature/moddle_point' (#11) from feature/moddle_point into develop
Reviewed-on: #11
2026-06-21 10:02:00 +00:00
a05fa5d0d7 build and format 2026-06-21 13:29:41 +03:30
69677a0a63 add middle points 2026-06-21 13:28:02 +03:30
b5c7501192 Merge tag 'v1.9.0' into develop
v1.9.0
2026-06-15 15:07:50 +03:30
42a0f3ad1c Merge branch 'release/v1.9.0' 2026-06-15 15:07:14 +03:30
0689fb307a release v1.9.0 2026-06-15 15:06:42 +03:30
09c7385faa Merge pull request 'bugfix/show_map_delay' (#10) from bugfix/show_map_delay into develop
Reviewed-on: #10
2026-06-15 11:31:17 +00:00
3745206604 build and format 2026-06-15 15:01:34 +03:30
f42a4ff63b send filter data for reports 2026-06-15 15:00:28 +03:30
18674a5b75 add delay to show routes 2026-06-15 11:48:23 +03:30
68e7fd8cd1 Merge pull request 'feature/end_km' (#9) from feature/end_km into develop
Reviewed-on: #9
2026-06-15 07:49:46 +00:00
e66af994af build and format 2026-06-15 11:19:39 +03:30
1e0d3dcf0e add end_km to violations 2026-06-15 11:16:34 +03:30
92e3505bd9 Merge pull request 'feature/machine_type_report' (#8) from feature/machine_type_report into develop
Reviewed-on: #8
2026-06-14 08:20:48 +00:00
241f583572 merge develop 2026-06-14 11:50:40 +03:30
3ea5c940c0 Merge branch 'develop' of http://git.witel.ir/RMS/frontend into feature/machine_type_report 2026-06-14 11:48:59 +03:30
806854327a build and format 2026-06-14 11:48:34 +03:30
a80e35a510 driver reports completed 2026-06-14 11:47:24 +03:30
cb85157cf7 Merge pull request 'add map to new mission on rowAction of operators' (#7) from feature/routing into develop
Reviewed-on: #7
2026-06-14 06:35:03 +00:00
a9a996e1e8 add map to new mission on rowAction of operators 2026-06-14 09:49:13 +03:30
92 changed files with 3960 additions and 1692 deletions

View File

@@ -1,5 +1,5 @@
HOST="rms.witel.ir" HOST="rms.witel.ir"
HOST_RMTO="rms.rmto.ir" HOST_RMTO="rms.rmto.ir"
NEXT_PUBLIC_VERSION="1.8.8" NEXT_PUBLIC_VERSION="1.9.1"
NEXT_PUBLIC_API_URL="https://rms.witel.ir" NEXT_PUBLIC_API_URL="https://rms.witel.ir"
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map" NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"

View File

@@ -0,0 +1,18 @@
import WithPermission from "@/core/middlewares/withPermission";
import DriverReportsPage from "@/components/dashboard/roadMissions/reports/diverReports";
export const metadata = {
title: "کارتابل گزارش راننده",
};
export default function Page() {
return (
<WithPermission
permission_name={[
"all", // TODO : change permission
]}
>
<DriverReportsPage />
</WithPermission>
);
}

View File

@@ -1,18 +1,18 @@
import WithPermission from "@/core/middlewares/withPermission"; import WithPermission from "@/core/middlewares/withPermission";
import MachinesTypeReportsPage from "@/components/dashboard/roadMissions/reports/machinesTypeReports"; import MachinesTypeReportsPage from "@/components/dashboard/roadMissions/reports/machinesTypeReports";
export const metadata = { export const metadata = {
title: "کارتابل گزارش نوع ماشین‌ها", title: "کارتابل گزارش نوع ماشین‌ها",
}; };
export default function Page() { export default function Page() {
return ( return (
<WithPermission <WithPermission
permission_name={[ permission_name={[
"all", // TODO : change permission "all", // TODO : change permission
]} ]}
> >
<MachinesTypeReportsPage /> <MachinesTypeReportsPage />
</WithPermission> </WithPermission>
); );
} }

View File

@@ -1,18 +1,18 @@
import MachinesReportsPage from "@/components/dashboard/roadMissions/reports/machinesReports"; import MachinesReportsPage from "@/components/dashboard/roadMissions/reports/machinesReports";
import WithPermission from "@/core/middlewares/withPermission"; import WithPermission from "@/core/middlewares/withPermission";
export const metadata = { export const metadata = {
title: "کارتابل گزارش ماشین‌ها", title: "کارتابل گزارش ماشین‌ها",
}; };
export default function Page() { export default function Page() {
return ( return (
<WithPermission <WithPermission
permission_name={[ permission_name={[
"all", // TODO : change permission "all", // TODO : change permission
]} ]}
> >
<MachinesReportsPage /> <MachinesReportsPage />
</WithPermission> </WithPermission>
); );
} }

View File

@@ -0,0 +1,20 @@
<svg width="30" height="35" viewBox="0 0 30 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4453 1.5127C24.4711 1.08082 31.4173 11.6767 27.2842 20.9209L27.0752 21.3672C25.9614 23.6466 24.4616 25.0302 23.0117 26.2031C21.6264 27.3238 20.4204 28.1673 19.3184 29.4482L19.0986 29.71C18.9038 29.9504 18.5282 30.5355 18.0273 31.2695C17.8006 31.6018 17.5566 31.95 17.332 32.2393C17.2202 32.3833 17.0974 32.5329 16.9727 32.666C16.8723 32.7731 16.6754 32.9748 16.4141 33.124L16.4131 33.123C15.7088 33.5252 14.941 33.6027 14.2256 33.3691C13.5676 33.1543 13.0766 32.7195 12.7383 32.3145V32.3135C12.3056 31.795 11.9245 31.2168 11.5986 30.7314C11.3403 30.3467 11.107 30.0062 10.8682 29.7012L10.626 29.4072C9.78615 28.4447 8.79047 27.6829 7.65723 26.7939C6.63272 25.9903 5.49745 25.0829 4.54102 23.9053L4.35254 23.666C-2.23923 15.0669 3.27657 2.37649 13.958 1.54199L14.4453 1.5127ZM14.4834 10.8291H14.4795C11.4118 11.1538 9.65384 14.8452 11.3496 17.6201L11.5254 17.8857L11.6885 18.1016C13.3725 20.1841 16.6502 20.1828 18.334 18.1006L18.4961 17.8857C20.762 14.6868 18.2266 10.4356 14.4834 10.8291Z" stroke="white" stroke-width="3"/>
<foreignObject x="-62" y="-62" width="154" height="159"></foreignObject><g filter="url(#filter0_d_225_89)">
<path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z" fill="#FFC107"/>
</g>
<defs>
<filter id="filter0_d_225_89" x="-62" y="-62" width="154" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_225_89"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_225_89" result="shape"/>
</filter>
<clipPath id="bgblur_0_225_89_clip_path" transform="translate(62 62)"><path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,21 @@
<svg width="42" height="66" viewBox="0 0 42 66" fill="#FFC107" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#FFC107"/>
<path clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="#FFC107"/>
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)">
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="#FFC107"/>
</g>
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
<defs>
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
</filter>
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
</clipPath></defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,50 +1,135 @@
import { Box, Button, TextField } from "@mui/material"; import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { const ROW_COLORS = {
source: "#0EA37A", // سبز
waypoint: "#F5A623", // زرد/نارنجی
dest: "#E53935", // قرمز
};
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: color,
flexShrink: 0,
}}
/>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
{label}
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: 12,
color: value ? "#1f1f1f" : "#bdbdbd",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{value || placeholder}
</Typography>
</Box>
{onEdit && (
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
</IconButton>
)}
{onRemove && (
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
</IconButton>
)}
</Stack>
);
const DataBox = ({
isSource,
isDest,
sourcePosition,
destPosition,
editingTarget,
onEditSource,
onEditDest,
waypoints = [],
onEditWaypoint,
onAddWaypoint,
onRemoveWaypoint,
canAddWaypoint,
}) => {
if (!isSource) return null; if (!isSource) return null;
return ( return (
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
bottom: 5, bottom: 12,
left: 5, left: 12,
zIndex: 1000, zIndex: 1000,
backgroundColor: "white", backgroundColor: "rgba(255,255,255,0.95)",
border: "1px solid #e1e1e1", backdropFilter: "blur(6px)",
padding: 1, border: "1px solid #ececec",
borderRadius: 2, borderRadius: 2,
opacity: 0.8, boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
gap: 1, px: 1.25,
display: "flex", py: 0.75,
flexDirection: "column", width: 230,
maxWidth: "70vw",
}} }}
> >
<div style={{ display: "flex", alignItems: "end", gap: 4 }}> <Row
<TextField color={ROW_COLORS.source}
variant="outlined" label="مبدا"
size="small" value={formatCoord(sourcePosition)}
readOnly onEdit={isSource && isDest ? onEditSource : null}
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""} />
{waypoints.map((wp, index) => (
<Row
key={`waypoint-${index}`}
color={ROW_COLORS.waypoint}
label={`نقطه میانی ${index + 1}`}
value={formatCoord(wp.position)}
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
onRemove={() => onRemoveWaypoint(index)}
/> />
{isSource && isDest && ( ))}
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && ( {isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}> <Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
<TextField )}
size="small"
variant="outlined" {canAddWaypoint && (
readOnly <>
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""} <Divider sx={{ my: 0.5 }} />
/> <Stack
<Button variant="contained" color="error" size="small" onClick={onEditDest}> direction="row"
ویرایش مقصد alignItems="center"
</Button> spacing={0.5}
</div> onClick={onAddWaypoint}
sx={{
cursor: "pointer",
color: ROW_COLORS.waypoint,
py: 0.25,
"&:hover": { opacity: 0.75 },
}}
>
<AddIcon sx={{ fontSize: 16 }} />
<Typography variant="caption" sx={{ fontWeight: 600 }}>
افزودن نقطه میانی
</Typography>
</Stack>
</>
)} )}
</Box> </Box>
); );

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Marker, Polyline, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import polyline from "@mapbox/polyline"; import polyline from "@mapbox/polyline";
@@ -18,14 +18,24 @@ const ShowRoute = ({ area }) => {
}); });
}; };
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
const [showRoute, setShowRoute] = useState(false);
useEffect(() => {
if (!coords.length) return;
setShowRoute(false);
map.flyToBounds(coords, {
padding: [50, 50],
duration: 1.5,
});
map.once("moveend", () => {
setShowRoute(true);
});
}, [area]);
if (!area) return null; if (!area) return null;
const coords = polyline.decode(area, 6); const coords = polyline.decode(area, 6);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
return ( return (
<> <>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} /> {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
<Marker position={sourcePosition} icon={sourceIcon} /> <Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} /> <Marker position={destPosition} icon={destIcon} />
</> </>
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
}; };
export default ShowRoute; export default ShowRoute;
// import { useEffect, useState } from "react";
// import { Marker, Polyline, useMap } from "react-leaflet";
// import L from "leaflet";
// import polyline from "@mapbox/polyline";
// import SourceIcon from "@/assets/images/source-icon.svg";
// import DestIcon from "@/assets/images/destination-icon.svg";
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
//
// const defaultIconSize = [35, 35];
//
// const createCustomIcon = (size, iconUrl) => {
// return L.icon({
// iconUrl: iconUrl,
// iconSize: size,
// iconAnchor: [size[0] / 2, size[1]],
// popupAnchor: [0, -size[1]],
// });
// };
//
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
//
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
// const ShowRoute = ({ area, locations = [] }) => {
// const map = useMap();
// const [showRoute, setShowRoute] = useState(false);
//
// const coords = area ? polyline.decode(area, 6) : [];
//
// useEffect(() => {
// if (!coords.length) return;
//
// setShowRoute(false);
//
// map.flyToBounds(coords, {
// padding: [50, 50],
// duration: 1.5,
// });
//
// map.once("moveend", () => {
// setShowRoute(true);
// });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [area]);
//
// if (!area || coords.length === 0) return null;
//
// const sourcePosition = coords[0];
// const destPosition = coords[coords.length - 1];
//
// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری)
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
//
// return (
// <>
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
//
// <Marker position={sourcePosition} icon={sourceIcon} />
//
// {middleWaypoints.map((wp, index) => (
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
// ))}
//
// <Marker position={destPosition} icon={destIcon} />
// </>
// );
// };
//
// export default ShowRoute;

View File

@@ -5,19 +5,29 @@ import SourceIcon from "@/assets/images/source-icon.svg";
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg"; import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg"; import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg"; import DestIcon from "@/assets/images/destination-icon.svg";
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox"; import DataBox from "./DataBox";
import AlarmText from "./AlarmText"; import AlarmText from "./AlarmText";
const MAX_WAYPOINTS = 2;
const Routing = ({ setArea, setLocations, locations, area }) => { const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef(); const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef(); const mapPrevDestMarker = useRef();
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint
const mapRef = useRef(null); const mapRef = useRef(null);
const defaultIconSize = [35, 35]; const defaultIconSize = [35, 35];
const [isSource, setIsSource] = useState(false); const [isSource, setIsSource] = useState(false);
const [sourcePosition, setSourcePosition] = useState(null); const [sourcePosition, setSourcePosition] = useState(null);
const [isDest, setIsDest] = useState(false); const [isDest, setIsDest] = useState(false);
const [destPosition, setDestPosition] = useState(null); const [destPosition, setDestPosition] = useState(null);
const [waypoints, setWaypoints] = useState([]);
const [routeCoords, setRouteCoords] = useState([]); const [routeCoords, setRouteCoords] = useState([]);
const [alternateCoords, setAlternateCoords] = useState([]); const [alternateCoords, setAlternateCoords] = useState([]);
const [editingTarget, setEditingTarget] = useState(null); const [editingTarget, setEditingTarget] = useState(null);
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
); );
const isMovingSource = !isSource || editingTarget === "source"; const getWaypointIcon = (index) => {
const isMovingDest = isSource && (!isDest || editingTarget === "dest"); const wp = waypoints[index];
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
};
const getActiveTarget = () => {
if (editingTarget) return editingTarget;
if (!isSource) return "source";
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
if (!isDest) return "dest";
return null;
};
const map = useMapEvents({ const map = useMapEvents({
move(e) { move(e) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); const target = getActiveTarget();
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); const center = e.target.getCenter();
if (target === "source") {
mapPrevSourceMarker.current?.setLatLng(center);
} else if (target === "dest") {
mapPrevDestMarker.current?.setLatLng(center);
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
}
}, },
movestart() { movestart() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); const target = getActiveTarget();
if (isMovingDest && mapPrevDestMarker.current) if (target === "source") {
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
} else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
}
}, },
moveend() { moveend() {
if (isMovingSource) const target = getActiveTarget();
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); if (target === "source") {
if (isMovingDest && mapPrevDestMarker.current) mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); } else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
}
}, },
}); });
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
useEffect(() => { useEffect(() => {
if (locations.length < 2) return; if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon }; const first = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon }; const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
setSourcePosition(restoredSource); const middle = locations.slice(1, -1).map((loc) => ({
setDestPosition(restoredDest); position: { lat: loc.lat, lng: loc.lon },
confirmed: true,
}));
setSourcePosition(first);
setDestPosition(last);
setWaypoints(middle);
setIsSource(true); setIsSource(true);
setIsDest(true); setIsDest(true);
if (area) { if (area) {
const decoded = polyline.decode(area, 6); const decoded = polyline.decode(area, 6);
setRouteCoords(decoded); setRouteCoords(decoded);
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100); }, 100);
} else { } else {
fetchRoute(restoredSource, restoredDest); fetchRoute(first, last, middle);
} }
}, []); }, []);
const fetchRoute = async (source, dest) => { const fetchRoute = async (source, dest, waypointsList = waypoints) => {
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
const locs = [ const locs = [
{ lat: source.lat, lon: source.lng }, { lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng }, { lat: dest.lat, lon: dest.lng },
]; ];
try { try {
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
} }
}; };
const resetRoute = () => {
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleMarkerClick = async () => { const handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") { if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng(); const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition); setSourcePosition(clickedPosition);
setIsSource(true); setIsSource(true);
const wasEditing = editingTarget === "source";
setEditingTarget(null); setEditingTarget(null);
setRouteCoords([]); resetRoute();
setAlternateCoords([]);
setSelectedRoute(null);
if (editingTarget === "source" && destPosition) { if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition); await fetchRoute(clickedPosition, destPosition, waypoints);
} }
} }
}; };
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
setDestPosition(clickedPosition); setDestPosition(clickedPosition);
setIsDest(true); setIsDest(true);
setEditingTarget(null); setEditingTarget(null);
resetRoute();
await fetchRoute(sourcePosition, clickedPosition); await fetchRoute(sourcePosition, clickedPosition, waypoints);
}
};
const handleWaypointMarkerClick = async (index) => {
const wp = waypoints[index];
if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت)
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
setWaypoints(updated);
const wasEditing = editingTarget === `waypoint-${index}`;
setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
} }
}; };
const handleEditSource = () => { const handleEditSource = () => {
setEditingTarget("source"); setEditingTarget("source");
setRouteCoords([]); resetRoute();
setAlternateCoords([]);
setSelectedRoute(null);
}; };
const handleEditDest = () => { const handleEditDest = () => {
setEditingTarget("dest"); setEditingTarget("dest");
setRouteCoords([]); resetRoute();
setAlternateCoords([]); };
setSelectedRoute(null);
const handleEditWaypoint = (index) => {
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
setEditingTarget(`waypoint-${index}`);
resetRoute();
};
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
const handleAddWaypoint = () => {
if (waypoints.length >= MAX_WAYPOINTS) return;
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
setEditingTarget(`waypoint-${waypoints.length}`);
resetRoute();
};
// حذف یک نقطه میانی
const handleRemoveWaypoint = async (index) => {
const updated = waypoints.filter((_, i) => i !== index);
setWaypoints(updated);
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
}
}; };
const handleSelectRoute = (index, coords) => { const handleSelectRoute = (index, coords) => {
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
editingTarget={editingTarget} editingTarget={editingTarget}
onEditSource={handleEditSource} onEditSource={handleEditSource}
onEditDest={handleEditDest} onEditDest={handleEditDest}
waypoints={waypoints}
onEditWaypoint={handleEditWaypoint}
onAddWaypoint={handleAddWaypoint}
onRemoveWaypoint={handleRemoveWaypoint}
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
/> />
<AlarmText isSource={isSource} /> <AlarmText isSource={isSource} />
<Marker <Marker
position={sourcePosition || map.getCenter()} position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker} ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }} eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon} icon={defaultIcon}
/> />
{waypoints.map((wp, index) => (
<Marker
key={`waypoint-${index}`}
position={wp.position || map.getCenter()}
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
icon={getWaypointIcon(index)}
/>
))}
{isSource && ( {isSource && (
<Marker <Marker
position={destPosition || map.getCenter()} position={destPosition || map.getCenter()}
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
icon={prevDestIcon} icon={prevDestIcon}
/> />
)} )}
{alternateCoords.map((coords, index) => ( {alternateCoords.map((coords, index) => (
<Polyline <Polyline
key={`alternate-${index}-${selectedRoute?.index === index}`} key={`alternate-${index}-${selectedRoute?.index === index}`}

View File

@@ -6,107 +6,107 @@ import { Controller, useWatch } from "react-hook-form";
const MissionDates = ({ control }) => { const MissionDates = ({ control }) => {
const type = useWatch({ control, name: "type" }); const type = useWatch({ control, name: "type" });
const start_date = useWatch({ control, name: "start_date" }); const start_date = useWatch({ control, name: "start_date" });
const end_date = useWatch({ control, name: "end_date" }); const end_date = useWatch({ control, name: "end_date" });
const start_time = useWatch({ control, name: "start_time" }); const start_time = useWatch({ control, name: "start_time" });
const end_time = useWatch({ control, name: "end_time" }); const end_time = useWatch({ control, name: "end_time" });
const startDateTime = makeDateTime(start_date, start_time);
const endDateTime = makeDateTime(end_date, end_time);
const now = new Date(); const now = new Date();
const startDateTime = start_date && start_time ? makeDateTime(start_date, start_time) : null;
const endDateTime = end_date && end_time ? makeDateTime(end_date, end_time) : null;
const isSameDay =
start_date && end_date && new Date(start_date).toDateString() === new Date(end_date).toDateString();
const isToday = start_date && new Date(start_date).toDateString() === now.toDateString();
return ( return (
type && ( type && (
<> <>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <Controller
control={control} control={control}
name={"start_date"} name="start_date"
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => (
return ( <MuiDatePicker
<MuiDatePicker name="start_date"
name="start_date" value={field.value}
error={error} error={error}
value={field.value} disableFuture={false}
disableFuture={false} minDate={now}
minDate={now} maxDate={end_date ?? null}
maxDate={end_date ?? null} label="تاریخ شروع ماموریت"
placeholder={"تاریخ شروع ماموریت را وارد کنید"} placeholder="تاریخ شروع ماموریت را وارد کنید"
label={"تاریخ شروع ماموریت"} setFieldValue={(name, value) => field.onChange(value || null)}
setFieldValue={(name, value) => field.onChange(value || [])} helperText={error?.message}
helperText={error ? error.message : null} />
/> )}
);
}}
/> />
</Grid> </Grid>
{type == 1 && ( {type == 1 && (
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <Controller
control={control} control={control}
name={"start_time"} name="start_time"
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => (
return ( <MuiTimePicker
<MuiTimePicker name="start_time"
name="start_time" value={field.value}
error={error} error={error}
value={field.value} minTime={isToday ? now : null}
minTime={ maxTime={isSameDay && endDateTime ? endDateTime : null}
start_date && new Date(start_date).toDateString() === now.toDateString() label="زمان شروع ماموریت"
? now placeholder="زمان شروع ماموریت را وارد کنید"
: null setFieldValue={(name, value) => field.onChange(value || null)}
} helperText={error?.message}
maxTime={endDateTime ?? null} />
placeholder={"زمان شروع ماموریت را وارد کنید"} )}
label={"زمان شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
/> />
</Grid> </Grid>
)} )}
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <Controller
control={control} control={control}
name={"end_date"} name="end_date"
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => (
return ( <MuiDatePicker
<MuiDatePicker name="end_date"
name="end_date" value={field.value}
error={error} error={error}
value={field.value} disableFuture={false}
disableFuture={false} minDate={start_date || now}
minDate={start_date ? start_date : now} label="تاریخ پایان ماموریت"
placeholder={"تاریخ پایان ماموریت را وارد کنید"} placeholder="تاریخ پایان ماموریت را وارد کنید"
label={"تاریخ پایان ماموریت"} setFieldValue={(name, value) => field.onChange(value || null)}
setFieldValue={(name, value) => field.onChange(value || [])} helperText={error?.message}
helperText={error ? error.message : null} />
/> )}
);
}}
/> />
</Grid> </Grid>
{type == 1 && ( {type == 1 && (
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <Controller
control={control} control={control}
name={"end_time"} name="end_time"
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => (
return ( <MuiTimePicker
<MuiTimePicker name="end_time"
name="end_time" value={field.value}
error={error} error={error}
value={field.value} minTime={isSameDay && startDateTime ? startDateTime : null}
minTime={startDateTime ?? null} label="زمان پایان ماموریت"
placeholder={"زمان پایان ماموریت را وارد کنید"} placeholder="زمان پایان ماموریت را وارد کنید"
label={"زمان پایان ماموریت"} setFieldValue={(name, value) => field.onChange(value || null)}
setFieldValue={(name, value) => field.onChange(value || null)} helperText={error?.message}
helperText={error ? error.message : null} />
/> )}
);
}}
/> />
</Grid> </Grid>
)} )}
@@ -114,4 +114,5 @@ const MissionDates = ({ control }) => {
) )
); );
}; };
export default MissionDates; export default MissionDates;

View File

@@ -0,0 +1,34 @@
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const ItemSelectField = ({ control, setValue }) => {
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
function getItemName(val) {
return itemsList.find((item) => item.id === val)?.name;
}
return (
<Controller
control={control}
name={"item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="موضوع"
selectors={itemsList}
isLoading={loadingItemsList}
schema={{ name: "name", value: "id" }}
error={errorItemsList}
helperText={error?.message}
onChange={(value) => {
field.onChange(value);
setValue("explanation", getItemName(value));
setValue("sub_item_id", "");
}}
/>
)}
/>
);
};
export default ItemSelectField;

View File

@@ -0,0 +1,37 @@
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
const itemId = watch("item_id");
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
function createExplanation(val) {
const currentExplanation = getValues("explanation");
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
return `${currentExplanation}-${selectedItem}`;
}
return (
<Controller
control={control}
name={"sub_item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="اقدام انجام شده"
selectors={subItemsList}
isLoading={loadingSubItemsList}
schema={{ name: "name", value: "id" }}
error={errorSubItemsList}
helperText={error?.message}
onChange={(val) => {
field.onChange(val);
setValue("explanation", createExplanation(val));
}}
/>
)}
/>
);
};
export default SubItemSelectField;

View File

@@ -9,12 +9,15 @@ import { ExitToApp } from "@mui/icons-material";
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { array, object, string } from "yup"; import { array, object, string } from "yup";
import ItemSelectField from "./ItemSelectField";
import SubItemSelectField from "./SubItemSelectField";
const validationSchema = object({ const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"),
end_point: string().required("مقصد را مشخص کنید!"), end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"), region: string().required("محور ماموریت را مشخص کنید!"),
category_id: string().required("نوع ماموریت را مشخص کنید!"), category_id: string().required("نوع ماموریت را مشخص کنید!"),
item_id: string().required("موضوع را مشخص کنید!"),
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"), requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"),
road_observed_id: string().when("category_id", { road_observed_id: string().when("category_id", {
is: "3", is: "3",
@@ -25,6 +28,8 @@ const validationSchema = object({
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = { const defaultValues = {
item_id: allData.item_id,
sub_item_id: allData.sub_item_id,
explanation: allData.explanation, explanation: allData.explanation,
end_point: allData.end_point, end_point: allData.end_point,
region: allData.region, region: allData.region,
@@ -33,7 +38,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
road_observed_id: allData.road_observed_id, road_observed_id: allData.road_observed_id,
}; };
const { control, handleSubmit } = useForm({ const { control, handleSubmit, watch, setValue, getValues } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
@@ -51,24 +56,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack> <Stack>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <ItemSelectField control={control} watch={watch} setValue={setValue} />
</Grid>
<Grid item xs={12} sm={6}>
<SubItemSelectField
control={control} control={control}
name={"explanation"} watch={watch}
render={({ field, fieldState: { error } }) => ( setValue={setValue}
<PersianTextField getValues={getValues}
value={field.value}
variant="outlined"
name="explanation"
onChange={field.onChange}
label="موضوع"
placeholder={"موضوع را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
)}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>

View File

@@ -44,6 +44,8 @@ const Create = ({ mutate }) => {
start_date: result.start_date, start_date: result.start_date,
end_date: result.end_date, end_date: result.end_date,
}), }),
item_id: result.item_id,
sub_item_id: result.sub_item_id,
}, },
hasSidebarUpdate: true, hasSidebarUpdate: true,
}) })
@@ -93,6 +95,8 @@ const Create = ({ mutate }) => {
defaultValues={{ defaultValues={{
explanation: "", explanation: "",
category_id: "", category_id: "",
item_id: null,
sub_item_id: null,
road_observed_id: "", road_observed_id: "",
rahdaran: [], rahdaran: [],
requested_machines: [], requested_machines: [],

View File

@@ -2,7 +2,7 @@ import { Close } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton } from "@mui/material"; import { Dialog, DialogTitle, IconButton } from "@mui/material";
import ViolationsList from "./ViolationsList"; import ViolationsList from "./ViolationsList";
const ViolationsDialog = ({ open, setOpen, mutate, row }) => { const ViolationsDialog = ({ open, setOpen, mutate }) => {
return ( return (
<Dialog open={open} fullWidth maxWidth="xs"> <Dialog open={open} fullWidth maxWidth="xs">
<IconButton <IconButton
@@ -19,7 +19,7 @@ const ViolationsDialog = ({ open, setOpen, mutate, row }) => {
<Close /> <Close />
</IconButton> </IconButton>
<DialogTitle>تخلفات</DialogTitle> <DialogTitle>تخلفات</DialogTitle>
{open && <ViolationsList setOpen={setOpen} />} {open && <ViolationsList setOpen={setOpen} mutate={mutate} />}
</Dialog> </Dialog>
); );
}; };

View File

@@ -1,50 +1,135 @@
import { Box, Button, TextField } from "@mui/material"; import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => { const ROW_COLORS = {
source: "#0EA37A", // سبز
waypoint: "#F5A623", // زرد/نارنجی
dest: "#E53935", // قرمز
};
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: color,
flexShrink: 0,
}}
/>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
{label}
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: 12,
color: value ? "#1f1f1f" : "#bdbdbd",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{value || placeholder}
</Typography>
</Box>
{onEdit && (
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
</IconButton>
)}
{onRemove && (
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
</IconButton>
)}
</Stack>
);
const DataBox = ({
isSource,
isDest,
sourcePosition,
destPosition,
editingTarget,
onEditSource,
onEditDest,
waypoints = [],
onEditWaypoint,
onAddWaypoint,
onRemoveWaypoint,
canAddWaypoint,
}) => {
if (!isSource) return null; if (!isSource) return null;
return ( return (
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
bottom: 5, bottom: 12,
left: 5, left: 12,
zIndex: 1000, zIndex: 1000,
backgroundColor: "white", backgroundColor: "rgba(255,255,255,0.95)",
border: "1px solid #e1e1e1", backdropFilter: "blur(6px)",
padding: 1, border: "1px solid #ececec",
borderRadius: 2, borderRadius: 2,
opacity: 0.8, boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
gap: 1, px: 1.25,
display: "flex", py: 0.75,
flexDirection: "column", width: 230,
maxWidth: "70vw",
}} }}
> >
<div style={{ display: "flex", alignItems: "end", gap: 4 }}> <Row
<TextField color={ROW_COLORS.source}
variant="outlined" label="مبدا"
size="small" value={formatCoord(sourcePosition)}
readOnly onEdit={isSource && isDest ? onEditSource : null}
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""} />
{waypoints.map((wp, index) => (
<Row
key={`waypoint-${index}`}
color={ROW_COLORS.waypoint}
label={`نقطه میانی ${index + 1}`}
value={formatCoord(wp.position)}
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
onRemove={() => onRemoveWaypoint(index)}
/> />
{isSource && isDest && ( ))}
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && ( {isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}> <Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
<TextField )}
size="small"
variant="outlined" {canAddWaypoint && (
readOnly <>
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""} <Divider sx={{ my: 0.5 }} />
/> <Stack
<Button variant="contained" color="error" size="small" onClick={onEditDest}> direction="row"
ویرایش مقصد alignItems="center"
</Button> spacing={0.5}
</div> onClick={onAddWaypoint}
sx={{
cursor: "pointer",
color: ROW_COLORS.waypoint,
py: 0.25,
"&:hover": { opacity: 0.75 },
}}
>
<AddIcon sx={{ fontSize: 16 }} />
<Typography variant="caption" sx={{ fontWeight: 600 }}>
افزودن نقطه میانی
</Typography>
</Stack>
</>
)} )}
</Box> </Box>
); );

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Marker, Polyline, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import polyline from "@mapbox/polyline"; import polyline from "@mapbox/polyline";
@@ -20,12 +20,22 @@ const ShowRoute = ({ area }) => {
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
useEffect(() => { const [showRoute, setShowRoute] = useState(false);
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
useEffect(() => {
if (!coords.length) return;
setShowRoute(false);
map.flyToBounds(coords, {
padding: [50, 50],
duration: 1.5,
});
map.once("moveend", () => {
setShowRoute(true);
});
}, [area]);
if (!area) return null; if (!area) return null;
const coords = polyline.decode(area, 6); const coords = polyline.decode(area, 6);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
return ( return (
<> <>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} /> {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
<Marker position={sourcePosition} icon={sourceIcon} /> <Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} /> <Marker position={destPosition} icon={destIcon} />
</> </>
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
}; };
export default ShowRoute; export default ShowRoute;
// import { useEffect, useState } from "react";
// import { Marker, Polyline, useMap } from "react-leaflet";
// import L from "leaflet";
// import polyline from "@mapbox/polyline";
// import SourceIcon from "@/assets/images/source-icon.svg";
// import DestIcon from "@/assets/images/destination-icon.svg";
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
//
// const defaultIconSize = [35, 35];
//
// const createCustomIcon = (size, iconUrl) => {
// return L.icon({
// iconUrl: iconUrl,
// iconSize: size,
// iconAnchor: [size[0] / 2, size[1]],
// popupAnchor: [0, -size[1]],
// });
// };
//
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
//
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
// const ShowRoute = ({ area, locations = [] }) => {
// const map = useMap();
// const [showRoute, setShowRoute] = useState(false);
//
// const coords = area ? polyline.decode(area, 6) : [];
//
// useEffect(() => {
// if (!coords.length) return;
//
// setShowRoute(false);
//
// map.flyToBounds(coords, {
// padding: [50, 50],
// duration: 1.5,
// });
//
// map.once("moveend", () => {
// setShowRoute(true);
// });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [area]);
//
// if (!area || coords.length === 0) return null;
//
// const sourcePosition = coords[0];
// const destPosition = coords[coords.length - 1];
//
// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری)
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
//
// return (
// <>
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
//
// <Marker position={sourcePosition} icon={sourceIcon} />
//
// {middleWaypoints.map((wp, index) => (
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
// ))}
//
// <Marker position={destPosition} icon={destIcon} />
// </>
// );
// };
//
// export default ShowRoute;

View File

@@ -5,19 +5,29 @@ import SourceIcon from "@/assets/images/source-icon.svg";
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg"; import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg"; import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg"; import DestIcon from "@/assets/images/destination-icon.svg";
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox"; import DataBox from "./DataBox";
import AlarmText from "./AlarmText"; import AlarmText from "./AlarmText";
const MAX_WAYPOINTS = 2;
const Routing = ({ setArea, setLocations, locations, area }) => { const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef(); const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef(); const mapPrevDestMarker = useRef();
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint
const mapRef = useRef(null); const mapRef = useRef(null);
const defaultIconSize = [35, 35]; const defaultIconSize = [35, 35];
const [isSource, setIsSource] = useState(false); const [isSource, setIsSource] = useState(false);
const [sourcePosition, setSourcePosition] = useState(null); const [sourcePosition, setSourcePosition] = useState(null);
const [isDest, setIsDest] = useState(false); const [isDest, setIsDest] = useState(false);
const [destPosition, setDestPosition] = useState(null); const [destPosition, setDestPosition] = useState(null);
const [waypoints, setWaypoints] = useState([]);
const [routeCoords, setRouteCoords] = useState([]); const [routeCoords, setRouteCoords] = useState([]);
const [alternateCoords, setAlternateCoords] = useState([]); const [alternateCoords, setAlternateCoords] = useState([]);
const [editingTarget, setEditingTarget] = useState(null); const [editingTarget, setEditingTarget] = useState(null);
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
); );
const isMovingSource = !isSource || editingTarget === "source"; const getWaypointIcon = (index) => {
const isMovingDest = isSource && (!isDest || editingTarget === "dest"); const wp = waypoints[index];
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
};
const getActiveTarget = () => {
if (editingTarget) return editingTarget;
if (!isSource) return "source";
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
if (!isDest) return "dest";
return null;
};
const map = useMapEvents({ const map = useMapEvents({
move(e) { move(e) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter()); const target = getActiveTarget();
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter()); const center = e.target.getCenter();
if (target === "source") {
mapPrevSourceMarker.current?.setLatLng(center);
} else if (target === "dest") {
mapPrevDestMarker.current?.setLatLng(center);
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
}
}, },
movestart() { movestart() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src)); const target = getActiveTarget();
if (isMovingDest && mapPrevDestMarker.current) if (target === "source") {
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src)); mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
} else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
}
}, },
moveend() { moveend() {
if (isMovingSource) const target = getActiveTarget();
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src)); if (target === "source") {
if (isMovingDest && mapPrevDestMarker.current) mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src)); } else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
}
}, },
}); });
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
useEffect(() => { useEffect(() => {
if (locations.length < 2) return; if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon }; const first = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon }; const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
setSourcePosition(restoredSource); const middle = locations.slice(1, -1).map((loc) => ({
setDestPosition(restoredDest); position: { lat: loc.lat, lng: loc.lon },
confirmed: true,
}));
setSourcePosition(first);
setDestPosition(last);
setWaypoints(middle);
setIsSource(true); setIsSource(true);
setIsDest(true); setIsDest(true);
if (area) { if (area) {
const decoded = polyline.decode(area, 6); const decoded = polyline.decode(area, 6);
setRouteCoords(decoded); setRouteCoords(decoded);
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 }); mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100); }, 100);
} else { } else {
fetchRoute(restoredSource, restoredDest); fetchRoute(first, last, middle);
} }
}, []); }, []);
const fetchRoute = async (source, dest) => { const fetchRoute = async (source, dest, waypointsList = waypoints) => {
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
const locs = [ const locs = [
{ lat: source.lat, lon: source.lng }, { lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng }, { lat: dest.lat, lon: dest.lng },
]; ];
try { try {
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
} }
}; };
const resetRoute = () => {
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleMarkerClick = async () => { const handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") { if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng(); const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition); setSourcePosition(clickedPosition);
setIsSource(true); setIsSource(true);
const wasEditing = editingTarget === "source";
setEditingTarget(null); setEditingTarget(null);
setRouteCoords([]); resetRoute();
setAlternateCoords([]);
setSelectedRoute(null);
if (editingTarget === "source" && destPosition) { if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition); await fetchRoute(clickedPosition, destPosition, waypoints);
} }
} }
}; };
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
setDestPosition(clickedPosition); setDestPosition(clickedPosition);
setIsDest(true); setIsDest(true);
setEditingTarget(null); setEditingTarget(null);
resetRoute();
await fetchRoute(sourcePosition, clickedPosition); await fetchRoute(sourcePosition, clickedPosition, waypoints);
}
};
const handleWaypointMarkerClick = async (index) => {
const wp = waypoints[index];
if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت)
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
setWaypoints(updated);
const wasEditing = editingTarget === `waypoint-${index}`;
setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
} }
}; };
const handleEditSource = () => { const handleEditSource = () => {
setEditingTarget("source"); setEditingTarget("source");
setRouteCoords([]); resetRoute();
setAlternateCoords([]);
setSelectedRoute(null);
}; };
const handleEditDest = () => { const handleEditDest = () => {
setEditingTarget("dest"); setEditingTarget("dest");
setRouteCoords([]); resetRoute();
setAlternateCoords([]); };
setSelectedRoute(null);
const handleEditWaypoint = (index) => {
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
setEditingTarget(`waypoint-${index}`);
resetRoute();
};
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
const handleAddWaypoint = () => {
if (waypoints.length >= MAX_WAYPOINTS) return;
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
setEditingTarget(`waypoint-${waypoints.length}`);
resetRoute();
};
// حذف یک نقطه میانی
const handleRemoveWaypoint = async (index) => {
const updated = waypoints.filter((_, i) => i !== index);
setWaypoints(updated);
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
}
}; };
const handleSelectRoute = (index, coords) => { const handleSelectRoute = (index, coords) => {
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
editingTarget={editingTarget} editingTarget={editingTarget}
onEditSource={handleEditSource} onEditSource={handleEditSource}
onEditDest={handleEditDest} onEditDest={handleEditDest}
waypoints={waypoints}
onEditWaypoint={handleEditWaypoint}
onAddWaypoint={handleAddWaypoint}
onRemoveWaypoint={handleRemoveWaypoint}
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
/> />
<AlarmText isSource={isSource} /> <AlarmText isSource={isSource} />
<Marker <Marker
position={sourcePosition || map.getCenter()} position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker} ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }} eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon} icon={defaultIcon}
/> />
{waypoints.map((wp, index) => (
<Marker
key={`waypoint-${index}`}
position={wp.position || map.getCenter()}
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
icon={getWaypointIcon(index)}
/>
))}
{isSource && ( {isSource && (
<Marker <Marker
position={destPosition || map.getCenter()} position={destPosition || map.getCenter()}
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
icon={prevDestIcon} icon={prevDestIcon}
/> />
)} )}
{alternateCoords.map((coords, index) => ( {alternateCoords.map((coords, index) => (
<Polyline <Polyline
key={`alternate-${index}-${selectedRoute?.index === index}`} key={`alternate-${index}-${selectedRoute?.index === index}`}

View File

@@ -0,0 +1,34 @@
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const ItemSelectField = ({ control, setValue }) => {
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
function getItemName(val) {
return itemsList.find((item) => item.id === val)?.name;
}
return (
<Controller
control={control}
name={"item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="موضوع"
selectors={itemsList}
isLoading={loadingItemsList}
schema={{ name: "name", value: "id" }}
error={errorItemsList}
helperText={error?.message}
onChange={(value) => {
field.onChange(value);
setValue("explanation", getItemName(value));
setValue("sub_item_id", "");
}}
/>
)}
/>
);
};
export default ItemSelectField;

View File

@@ -0,0 +1,37 @@
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
const itemId = watch("item_id");
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
function createExplanation(val) {
const currentExplanation = getValues("explanation");
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
return `${currentExplanation}-${selectedItem}`;
}
return (
<Controller
control={control}
name={"sub_item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="اقدام انجام شده"
selectors={subItemsList}
isLoading={loadingSubItemsList}
schema={{ name: "name", value: "id" }}
error={errorSubItemsList}
helperText={error?.message}
onChange={(val) => {
field.onChange(val);
setValue("explanation", createExplanation(val));
}}
/>
)}
/>
);
};
export default SubItemSelectField;

View File

@@ -8,10 +8,12 @@ import { ExitToApp } from "@mui/icons-material";
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup"; import { object, string } from "yup";
import FastReactCode from "./FastReactCode"; import ItemSelectField from "./ItemSelectField";
import SubItemSelectField from "./SubItemSelectField";
const validationSchema = object({ const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"), item_id: string().required("موضوع را مشخص کنید!"),
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
end_point: string().required("مقصد را مشخص کنید!"), end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"), region: string().required("محور ماموریت را مشخص کنید!"),
category_id: string().required("نوع ماموریت را مشخص کنید!"), category_id: string().required("نوع ماموریت را مشخص کنید!"),
@@ -24,6 +26,8 @@ const validationSchema = object({
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = { const defaultValues = {
item_id: allData.item_id,
sub_item_id: allData.sub_item_id,
explanation: allData.explanation, explanation: allData.explanation,
end_point: allData.end_point, end_point: allData.end_point,
region: allData.region, region: allData.region,
@@ -31,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
road_observed_id: allData.road_observed_id, road_observed_id: allData.road_observed_id,
}; };
const { control, handleSubmit } = useForm({ const { control, handleSubmit, watch, setValue, getValues } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
@@ -49,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack> <Stack>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <ItemSelectField control={control} watch={watch} setValue={setValue} />
</Grid>
<Grid item xs={12} sm={6}>
<SubItemSelectField
control={control} control={control}
name={"explanation"} watch={watch}
render={({ field, fieldState: { error } }) => ( setValue={setValue}
<PersianTextField getValues={getValues}
value={field.value}
variant="outlined"
name="explanation"
onChange={field.onChange}
label="موضوع"
placeholder={"موضوع را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
)}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>

View File

@@ -20,10 +20,7 @@ const MachineAndDriver = ({ setTabState, allData, setAllData, handlePrev }) => {
<> <>
<Typography variant="body2">راننده</Typography> <Typography variant="body2">راننده</Typography>
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
<Chip <Chip label={driver ? `${driver.name}` : "راننده ای انتخاب نشده"} size="small" />
label={driver ? `${driver.code} | ${driver.name}` : "راننده ای انتخاب نشده"}
size="small"
/>
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
<DriversDialog setDriver={setDriver} mode={"edit"} /> <DriversDialog setDriver={setDriver} mode={"edit"} />
</> </>

View File

@@ -25,17 +25,17 @@ const RahdaranList = ({ rahdaran, setRahdaran }) => {
my: 1, my: 1,
}} }}
> >
<Stack direction={"row"} spacing={1}> <Stack direction={"row"} sx={{ display: "flex", alignItems: "center" }} spacing={1}>
<AccountCircle color="primary" sx={{ width: "32px", height: "32px" }} /> <AccountCircle color="primary" sx={{ width: "32px", height: "32px" }} />
<Stack> <Stack>
<Stack direction={"row"} spacing={0.5}> <Stack direction={"row"} spacing={0.5}>
<Typography variant="caption">نام و نام خانوادگی:</Typography> <Typography variant="caption">نام و نام خانوادگی:</Typography>
<Typography variant="body2">{rahdar.name}</Typography> <Typography variant="body2">{rahdar.name}</Typography>
</Stack> </Stack>
<Stack direction={"row"} spacing={0.5}> {/*<Stack direction={"row"} spacing={0.5}>*/}
<Typography variant="caption">کدملی:</Typography> {/* <Typography variant="caption">کدملی:</Typography>*/}
<Typography variant="body2">{rahdar.code}</Typography> {/* <Typography variant="body2">-</Typography> /!*{rahdar.code}*!/*/}
</Stack> {/*</Stack>*/}
</Stack> </Stack>
</Stack> </Stack>
<IconButton color="error" onClick={() => handleRemove(index)}> <IconButton color="error" onClick={() => handleRemove(index)}>

View File

@@ -1,12 +1,11 @@
import { AccessTime, Engineering, InsertDriveFile, Person, Verified, Map } from "@mui/icons-material"; import { Engineering, InsertDriveFile, Map, Person, Verified } from "@mui/icons-material";
import { Box, Tab, Tabs } from "@mui/material"; import { Box, Tab, Tabs } from "@mui/material";
import { useReducer, useState } from "react"; import { useReducer, useState } from "react";
import GetDateTime from "./GetDateTime"; import Area from "./Area";
import GetItemInfo from "./GetItemInfo"; import GetItemInfo from "./GetItemInfo";
import MachineAndDriver from "./MachineAndDriver"; import MachineAndDriver from "./MachineAndDriver";
import Rahdaran from "./Rahdaran"; import Rahdaran from "./Rahdaran";
import Verify from "./Verify"; import Verify from "./Verify";
import Area from "./Area";
function TabPanel(props) { function TabPanel(props) {
const { children, value, index } = props; const { children, value, index } = props;

View File

@@ -5,7 +5,7 @@ import { Dialog, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import CreateForm from "./Form"; import CreateForm from "./Form";
const MissionCorrection = ({ row, mutate }) => { const MissionCorrection = ({ row, mutate, baseMutate }) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -26,11 +26,14 @@ const MissionCorrection = ({ row, mutate }) => {
end_point: result.end_point, end_point: result.end_point,
driver: result.driver.id, driver: result.driver.id,
encoded_route: result.area, encoded_route: result.area,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
}, },
hasSidebarUpdate: true, hasSidebarUpdate: true,
}) })
.then((response) => { .then((response) => {
mutate(); mutate();
baseMutate();
setOpen(false); setOpen(false);
}) })
.catch((error) => {}) .catch((error) => {})

View File

@@ -1,9 +1,9 @@
import MissionCorrection from "./MissionCorrection"; import MissionCorrection from "./MissionCorrection";
export default function RowActions({ row, mutate }) { export default function RowActions({ row, mutate, baseMutate }) {
return ( return (
<> <>
<MissionCorrection row={row} mutate={mutate} /> <MissionCorrection row={row} mutate={mutate} baseMutate={baseMutate} />
</> </>
); );
} }

View File

@@ -7,7 +7,7 @@ import useCities from "@/lib/hooks/useCities";
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency"; import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import useProvinces from "@/lib/hooks/useProvince"; import useProvinces from "@/lib/hooks/useProvince";
const ViolationsList = ({ setOpen }) => { const ViolationsList = ({ setOpen, mutate }) => {
const columns = useMemo(() => { const columns = useMemo(() => {
const dynamicColumns = { const dynamicColumns = {
header: "استان", header: "استان",
@@ -132,7 +132,7 @@ const ViolationsList = ({ setOpen }) => {
table_name={"operatorViolationsList"} table_name={"operatorViolationsList"}
enableRowActions enableRowActions
positionActionsColumn={"first"} positionActionsColumn={"first"}
RowActions={(props) => <RowActions {...props} setOpen={setOpen} />} RowActions={(props) => <RowActions {...props} setOpen={setOpen} baseMutate={mutate} />}
/> />
</Box> </Box>
</> </>

View File

@@ -4,7 +4,7 @@ import ViolationsDialog from "./Dialog";
import theme from "@/core/utils/theme"; import theme from "@/core/utils/theme";
import RemoveRoadIcon from "@mui/icons-material/RemoveRoad"; import RemoveRoadIcon from "@mui/icons-material/RemoveRoad";
const OperatorViolations = ({ row, mutate }) => { const OperatorViolations = ({ mutate }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
return ( return (
@@ -26,7 +26,7 @@ const OperatorViolations = ({ row, mutate }) => {
</Button> </Button>
)} )}
</Tooltip> </Tooltip>
<ViolationsDialog open={open} setOpen={setOpen} mutate={mutate} row={row} /> <ViolationsDialog open={open} setOpen={setOpen} mutate={mutate} />
</> </>
); );
}; };

View File

@@ -0,0 +1,28 @@
import { Box, Typography } from "@mui/material";
const AlarmText = ({ isSource }) => {
if (isSource) return null;
return (
<Box
sx={{
position: "absolute",
top: 5,
left: 5,
zIndex: 1000,
border: "1px solid #ed0f02",
backgroundColor: "#f19898",
py: 0.5,
px: 1,
borderRadius: 2,
opacity: 0.8,
boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px",
}}
>
<Typography variant="caption" sx={{ fontWeight: "bold" }} color="#ed0f02">
با کلیک بر روی مارکر موقعیت را انتخاب کنید
</Typography>
</Box>
);
};
export default AlarmText;

View File

@@ -0,0 +1,138 @@
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
const ROW_COLORS = {
source: "#0EA37A", // سبز
waypoint: "#F5A623", // زرد/نارنجی
dest: "#E53935", // قرمز
};
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: color,
flexShrink: 0,
}}
/>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
{label}
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: 12,
color: value ? "#1f1f1f" : "#bdbdbd",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{value || placeholder}
</Typography>
</Box>
{onEdit && (
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
</IconButton>
)}
{onRemove && (
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
</IconButton>
)}
</Stack>
);
const DataBox = ({
isSource,
isDest,
sourcePosition,
destPosition,
editingTarget,
onEditSource,
onEditDest,
waypoints = [],
onEditWaypoint,
onAddWaypoint,
onRemoveWaypoint,
canAddWaypoint,
}) => {
if (!isSource) return null;
return (
<Box
sx={{
position: "absolute",
bottom: 12,
left: 12,
zIndex: 1000,
backgroundColor: "rgba(255,255,255,0.95)",
backdropFilter: "blur(6px)",
border: "1px solid #ececec",
borderRadius: 2,
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
px: 1.25,
py: 0.75,
width: 230,
maxWidth: "70vw",
}}
>
<Row
color={ROW_COLORS.source}
label="مبدا"
value={formatCoord(sourcePosition)}
onEdit={isSource && isDest ? onEditSource : null}
/>
{waypoints.map((wp, index) => (
<Row
key={`waypoint-${index}`}
color={ROW_COLORS.waypoint}
label={`نقطه میانی ${index + 1}`}
value={formatCoord(wp.position)}
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
onRemove={() => onRemoveWaypoint(index)}
/>
))}
{isDest && (
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
)}
{canAddWaypoint && (
<>
<Divider sx={{ my: 0.5 }} />
<Stack
direction="row"
alignItems="center"
spacing={0.5}
onClick={onAddWaypoint}
sx={{
cursor: "pointer",
color: ROW_COLORS.waypoint,
py: 0.25,
"&:hover": { opacity: 0.75 },
}}
>
<AddIcon sx={{ fontSize: 16 }} />
<Typography variant="caption" sx={{ fontWeight: 600 }}>
افزودن نقطه میانی
</Typography>
</Stack>
</>
)}
</Box>
);
};
export default DataBox;

View File

@@ -0,0 +1,128 @@
import { useEffect, useRef, useState } from "react";
import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet";
import polyline from "@mapbox/polyline";
import SourceIcon from "@/assets/images/source-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg";
const ShowRoute = ({ area }) => {
const map = useMap();
const defaultIconSize = [35, 35];
const createCustomIcon = (size, iconUrl) => {
return L.icon({
iconUrl: iconUrl,
iconSize: size,
iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]],
});
};
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
const [showRoute, setShowRoute] = useState(false);
useEffect(() => {
if (!coords.length) return;
setShowRoute(false);
map.flyToBounds(coords, {
padding: [50, 50],
duration: 1.5,
});
map.once("moveend", () => {
setShowRoute(true);
});
}, [area]);
if (!area) return null;
const coords = polyline.decode(area, 6);
if (coords.length === 0) return null;
const sourcePosition = coords[0];
const destPosition = coords[coords.length - 1];
return (
<>
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
<Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} />
</>
);
};
export default ShowRoute;
// import { useEffect, useState } from "react";
// import { Marker, Polyline, useMap } from "react-leaflet";
// import L from "leaflet";
// import polyline from "@mapbox/polyline";
// import SourceIcon from "@/assets/images/source-icon.svg";
// import DestIcon from "@/assets/images/destination-icon.svg";
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
//
// const defaultIconSize = [35, 35];
//
// const createCustomIcon = (size, iconUrl) => {
// return L.icon({
// iconUrl: iconUrl,
// iconSize: size,
// iconAnchor: [size[0] / 2, size[1]],
// popupAnchor: [0, -size[1]],
// });
// };
//
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
//
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
// const ShowRoute = ({ area, locations = [] }) => {
// const map = useMap();
// const [showRoute, setShowRoute] = useState(false);
//
// const coords = area ? polyline.decode(area, 6) : [];
//
// useEffect(() => {
// if (!coords.length) return;
//
// setShowRoute(false);
//
// map.flyToBounds(coords, {
// padding: [50, 50],
// duration: 1.5,
// });
//
// map.once("moveend", () => {
// setShowRoute(true);
// });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [area]);
//
// if (!area || coords.length === 0) return null;
//
// const sourcePosition = coords[0];
// const destPosition = coords[coords.length - 1];
//
// // نقاط میانی فقط از روی locations قابل استخراج‌ان (بین اولی و آخری)
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
//
// return (
// <>
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
//
// <Marker position={sourcePosition} icon={sourceIcon} />
//
// {middleWaypoints.map((wp, index) => (
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
// ))}
//
// <Marker position={destPosition} icon={destIcon} />
// </>
// );
// };
//
// export default ShowRoute;

View File

@@ -0,0 +1,334 @@
import { Marker, Polyline, useMapEvents } from "react-leaflet";
import L from "leaflet";
import polyline from "@mapbox/polyline";
import SourceIcon from "@/assets/images/source-icon.svg";
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg";
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
import { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox";
import AlarmText from "./AlarmText";
const MAX_WAYPOINTS = 2;
const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef();
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی به‌ازای هر waypoint
const mapRef = useRef(null);
const defaultIconSize = [35, 35];
const [isSource, setIsSource] = useState(false);
const [sourcePosition, setSourcePosition] = useState(null);
const [isDest, setIsDest] = useState(false);
const [destPosition, setDestPosition] = useState(null);
const [waypoints, setWaypoints] = useState([]);
const [routeCoords, setRouteCoords] = useState([]);
const [alternateCoords, setAlternateCoords] = useState([]);
const [editingTarget, setEditingTarget] = useState(null);
const [selectedRoute, setSelectedRoute] = useState(null);
const createCustomIcon = (size, iconUrl) => {
return L.icon({
iconUrl: iconUrl,
iconSize: size,
iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]],
});
};
const defaultIcon = createCustomIcon(
defaultIconSize,
isSource && editingTarget !== "source" ? SourceIcon.src : PrevSourceIcon.src
);
const prevDestIcon = createCustomIcon(
defaultIconSize,
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
);
const getWaypointIcon = (index) => {
const wp = waypoints[index];
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
};
const getActiveTarget = () => {
if (editingTarget) return editingTarget;
if (!isSource) return "source";
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
if (!isDest) return "dest";
return null;
};
const map = useMapEvents({
move(e) {
const target = getActiveTarget();
const center = e.target.getCenter();
if (target === "source") {
mapPrevSourceMarker.current?.setLatLng(center);
} else if (target === "dest") {
mapPrevDestMarker.current?.setLatLng(center);
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
}
},
movestart() {
const target = getActiveTarget();
if (target === "source") {
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
} else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
}
},
moveend() {
const target = getActiveTarget();
if (target === "source") {
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
} else if (target === "dest") {
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
} else if (target?.startsWith("waypoint-")) {
const idx = Number(target.split("-")[1]);
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
}
},
});
useEffect(() => {
mapRef.current = map;
}, [map]);
useEffect(() => {
if (locations.length < 2) return;
const first = { lat: locations[0].lat, lng: locations[0].lon };
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
const middle = locations.slice(1, -1).map((loc) => ({
position: { lat: loc.lat, lng: loc.lon },
confirmed: true,
}));
setSourcePosition(first);
setDestPosition(last);
setWaypoints(middle);
setIsSource(true);
setIsDest(true);
if (area) {
const decoded = polyline.decode(area, 6);
setRouteCoords(decoded);
setSelectedRoute({ index: "main", coords: decoded });
setTimeout(() => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100);
} else {
fetchRoute(first, last, middle);
}
}, []);
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
const locs = [
{ lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng },
];
try {
const response = await fetch("https://jouya.141.ir/api/router/drive", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ locations: locs }),
});
const data = await response.json();
const shape = data.trip.legs[0].shape;
setLocations(locs);
setArea(shape);
const decoded = polyline.decode(shape, 6);
setRouteCoords(decoded);
setSelectedRoute({ index: "main", coords: decoded });
if (data.alternates?.length > 0) {
const decodedAlternates = data.alternates.map((alt) => polyline.decode(alt.trip.legs[0].shape, 6));
setAlternateCoords(decodedAlternates);
} else {
setAlternateCoords([]);
}
map.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
} catch (error) {
console.error("Routing error:", error);
}
};
const resetRoute = () => {
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition);
setIsSource(true);
const wasEditing = editingTarget === "source";
setEditingTarget(null);
resetRoute();
if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition, waypoints);
}
}
};
const handleDestMarkerClick = async () => {
if (!isDest || editingTarget === "dest") {
const clickedPosition = mapPrevDestMarker.current.getLatLng();
setDestPosition(clickedPosition);
setIsDest(true);
setEditingTarget(null);
resetRoute();
await fetchRoute(sourcePosition, clickedPosition, waypoints);
}
};
const handleWaypointMarkerClick = async (index) => {
const wp = waypoints[index];
if (!wp || wp.confirmed) return; // فقط روی نقطه‌ی تأیید‌نشده کلیک معتبره (یا حالت ادیت)
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
setWaypoints(updated);
const wasEditing = editingTarget === `waypoint-${index}`;
setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
}
};
const handleEditSource = () => {
setEditingTarget("source");
resetRoute();
};
const handleEditDest = () => {
setEditingTarget("dest");
resetRoute();
};
const handleEditWaypoint = (index) => {
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed می‌کنیم تا با حرکت نقشه جابه‌جا بشه
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
setEditingTarget(`waypoint-${index}`);
resetRoute();
};
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
const handleAddWaypoint = () => {
if (waypoints.length >= MAX_WAYPOINTS) return;
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
setEditingTarget(`waypoint-${waypoints.length}`);
resetRoute();
};
// حذف یک نقطه میانی
const handleRemoveWaypoint = async (index) => {
const updated = waypoints.filter((_, i) => i !== index);
setWaypoints(updated);
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
resetRoute();
if (sourcePosition && destPosition) {
await fetchRoute(sourcePosition, destPosition, updated);
}
};
const handleSelectRoute = (index, coords) => {
setSelectedRoute({ index, coords });
const encoded = polyline.encode(coords, 6);
setArea(encoded);
map.flyToBounds(coords, { padding: [50, 50], duration: 0.3 });
};
return (
<>
<DataBox
isSource={isSource}
isDest={isDest}
sourcePosition={sourcePosition}
destPosition={destPosition}
editingTarget={editingTarget}
onEditSource={handleEditSource}
onEditDest={handleEditDest}
waypoints={waypoints}
onEditWaypoint={handleEditWaypoint}
onAddWaypoint={handleAddWaypoint}
onRemoveWaypoint={handleRemoveWaypoint}
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
/>
<AlarmText isSource={isSource} />
<Marker
position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon}
/>
{waypoints.map((wp, index) => (
<Marker
key={`waypoint-${index}`}
position={wp.position || map.getCenter()}
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
icon={getWaypointIcon(index)}
/>
))}
{isSource && (
<Marker
position={destPosition || map.getCenter()}
ref={mapPrevDestMarker}
eventHandlers={{ click: handleDestMarkerClick }}
icon={prevDestIcon}
/>
)}
{alternateCoords.map((coords, index) => (
<Polyline
key={`alternate-${index}-${selectedRoute?.index === index}`}
positions={coords}
color={selectedRoute?.index === index ? "#015688" : "#91B2C6"}
weight={selectedRoute?.index === index ? 7 : 6}
opacity={selectedRoute?.index === index ? 1 : 0.7}
eventHandlers={{ click: () => handleSelectRoute(index, coords) }}
/>
))}
{routeCoords.length > 0 && (
<Polyline
key={`main-${selectedRoute?.index === "main"}`}
positions={routeCoords}
color={selectedRoute?.index === "main" ? "#015688" : "#91B2C6"}
weight={selectedRoute?.index === "main" ? 7 : 6}
opacity={selectedRoute?.index === "main" ? 1 : 0.7}
eventHandlers={{ click: () => handleSelectRoute("main", routeCoords) }}
/>
)}
</>
);
};
export default Routing;

View File

@@ -2,41 +2,29 @@ import MapLoading from "@/core/components/MapLayer/Loading";
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import MapControlPolygon from "./MapControlPolygon"; import Routing from "./Routing";
import MapControlPolyline from "./MapControlPolyline";
import SelectBoundType from "./SelectBoundType";
import OldBoundLayer from "./OldBoundLayer";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), { const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />, loading: () => <MapLoading />,
ssr: false, ssr: false,
}); });
const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => { const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
const [bound, setBound] = useState(allData.bound); const [area, setArea] = useState(allData.area);
const [boundType, setBoundType] = useState(allData.bound_type); const [locations, setLocations] = useState(allData.locations);
const handleNext = useCallback(() => { const handleNext = useCallback(() => {
setAllData({ bound: bound }); setAllData({ area: area });
setAllData({ locations: locations });
setTabState((s) => s + 1); setTabState((s) => s + 1);
}, [bound]); }, [area]);
return ( return (
<> <>
<DialogContent dividers> <DialogContent dividers>
<Stack spacing={2}> <Stack spacing={2}>
<SelectBoundType
boundType={boundType}
setBoundType={setBoundType}
setBound={setBound}
setAllData={setAllData}
/>
<Box sx={{ width: "100%", height: "400px" }}> <Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}> <MapLayer style={{ borderRadius: "4px" }}>
{boundType == "polygon" ? ( <Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
<MapControlPolygon bound={bound} setBound={setBound} boundType={boundType} />
) : (
<MapControlPolyline bound={bound} setBound={setBound} boundType={boundType} />
)}
<OldBoundLayer bound={oldBound} />
</MapLayer> </MapLayer>
</Box> </Box>
</Stack> </Stack>
@@ -45,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large"> <Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"} {"مرحله قبلی"}
</Button> </Button>
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}> <Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
{!bound ? "در انتظار ترسیم" : "مرحله بعد"} {!area ? "در انتظار ترسیم" : "مرحله بعد"}
</Button> </Button>
</DialogActions> </DialogActions>
</> </>

View File

@@ -0,0 +1,34 @@
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const ItemSelectField = ({ control, setValue }) => {
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
function getItemName(val) {
return itemsList.find((item) => item.id === val)?.name;
}
return (
<Controller
control={control}
name={"item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="موضوع"
selectors={itemsList}
isLoading={loadingItemsList}
schema={{ name: "name", value: "id" }}
error={errorItemsList}
helperText={error?.message}
onChange={(value) => {
field.onChange(value);
setValue("explanation", getItemName(value));
setValue("sub_item_id", "");
}}
/>
)}
/>
);
};
export default ItemSelectField;

View File

@@ -0,0 +1,37 @@
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
import { Controller } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
const itemId = watch("item_id");
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
function createExplanation(val) {
const currentExplanation = getValues("explanation");
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
return `${currentExplanation}-${selectedItem}`;
}
return (
<Controller
control={control}
name={"sub_item_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="اقدام انجام شده"
selectors={subItemsList}
isLoading={loadingSubItemsList}
schema={{ name: "name", value: "id" }}
error={errorSubItemsList}
helperText={error?.message}
onChange={(val) => {
field.onChange(val);
setValue("explanation", createExplanation(val));
}}
/>
)}
/>
);
};
export default SubItemSelectField;

View File

@@ -1,3 +1,4 @@
import LtrTextField from "@/core/components/LtrTextField";
import PersianTextField from "@/core/components/PersianTextField"; import PersianTextField from "@/core/components/PersianTextField";
import SelectBox from "@/core/components/SelectBox"; import SelectBox from "@/core/components/SelectBox";
import StyledForm from "@/core/components/StyledForm"; import StyledForm from "@/core/components/StyledForm";
@@ -8,6 +9,8 @@ import { ExitToApp } from "@mui/icons-material";
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup"; import { object, string } from "yup";
import ItemSelectField from "./ItemSelectField";
import SubItemSelectField from "./SubItemSelectField";
const validationSchema = object({ const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"), explanation: string().required("موضوع را مشخص کنید!"),
@@ -23,6 +26,8 @@ const validationSchema = object({
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => { const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = { const defaultValues = {
item_id: allData.item_id,
sub_item_id: allData.sub_item_id,
explanation: allData.explanation, explanation: allData.explanation,
end_point: allData.end_point, end_point: allData.end_point,
region: allData.region, region: allData.region,
@@ -30,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
road_observed_id: allData.road_observed_id, road_observed_id: allData.road_observed_id,
}; };
const { control, handleSubmit } = useForm({ const { control, handleSubmit, watch, setValue, getValues } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
@@ -48,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack> <Stack>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller <ItemSelectField control={control} watch={watch} setValue={setValue} />
</Grid>
<Grid item xs={12} sm={6}>
<SubItemSelectField
control={control} control={control}
name={"explanation"} watch={watch}
render={({ field, fieldState: { error } }) => ( setValue={setValue}
<PersianTextField getValues={getValues}
value={field.value}
variant="outlined"
name="explanation"
onChange={field.onChange}
label="موضوع"
placeholder={"موضوع را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
)}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
@@ -125,6 +120,29 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={6} sm={6}>
<Stack>
<Controller
control={control}
name={"end_km"}
render={({ field, fieldState: { error } }) => (
<LtrTextField
autoComplete="off"
type="tel"
value={field.value}
onChange={(e) => {
if (isNaN(Number(e.target.value))) return;
field.onChange(e.target.value);
}}
size="small"
fullWidth
label="کیلومتر خودرو / ساعت کار"
InputLabelProps={{ shrink: true }}
/>
)}
/>
</Stack>
</Grid>
</Grid> </Grid>
</Stack> </Stack>
</Box> </Box>

View File

@@ -5,7 +5,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr
import moment from "jalali-moment"; import moment from "jalali-moment";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useCallback } from "react"; import { useCallback } from "react";
import ShowBound from "../../../../../../Actions/showBound"; import ShowRoute from "../Area/Routing/ShowRoute";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), { const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />, loading: () => <MapLoading />,
ssr: false, ssr: false,
@@ -23,7 +23,7 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Stack spacing={2}> <Stack spacing={2}>
<Box sx={{ width: "100%", height: "200px" }}> <Box sx={{ width: "100%", height: "200px" }}>
<MapLayer style={{ borderRadius: "4px" }}> <MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={allData.bound} /> <ShowRoute area={allData.area} />
</MapLayer> </MapLayer>
</Box> </Box>
<Stack spacing={1}> <Stack spacing={1}>
@@ -60,6 +60,11 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
<Chip size="small" label={allData.end_point} /> <Chip size="small" label={allData.end_point} />
</Stack> </Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">کیلومتر خودرو / ساعت کار</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={allData.end_km} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}> <Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ شروع ماموریت</Typography> <Typography variant="body2">تاریخ شروع ماموریت</Typography>
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
@@ -88,16 +93,18 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Stack direction={"row"} alignItems={"center"} spacing={1}> <Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">خودرو</Typography> <Typography variant="body2">خودرو</Typography>
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
<Chip {allData.machine && (
size="small" <Chip
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`} size="small"
/> label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
/>
)}
</Stack> </Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}> {/* <Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">راننده</Typography> <Typography variant="body2">راننده</Typography>
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
<Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} /> <Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} />
</Stack> </Stack> */}
</Stack> </Stack>
<Stack spacing={1}> <Stack spacing={1}>
<Divider> <Divider>

View File

@@ -84,7 +84,6 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting, oldBound }
</TabPanel> </TabPanel>
<TabPanel value={tabState} index={2}> <TabPanel value={tabState} index={2}>
<Area <Area
oldBound={oldBound}
allData={allData} allData={allData}
setAllData={(data) => { setAllData={(data) => {
dispatch({ type: "changeData", data }); dispatch({ type: "changeData", data });

View File

@@ -9,25 +9,18 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const submitForm = async (result) => { const submitForm = async (result) => {
setSubmitting(true); console.log("result: ", result);
const bound = result.bound.getLatLngs();
let area =
result.bound_type === "polygon"
? bound.map((ring) => ring.map((latlng) => [latlng.lng, latlng.lat]))[0]
: bound.map((latlng) => [latlng.lng, latlng.lat]);
// بستن polygon setSubmitting(true);
if (result.bound_type === "polygon" && area.length > 0) {
const firstPoint = area[0];
area.push({ ...firstPoint });
}
await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", { await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", {
data: { data: {
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}), ...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
machine_id: result.machine.id, machine_id: result.machine.id,
driver: result.driver.id, encoded_route: result.area,
driver: row.original.driver_id,
zone: result.region, zone: result.region,
type: result.type, type: result.type,
end_km: result.end_km,
...(result.type == 1 ...(result.type == 1
? { ? {
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`, end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
@@ -36,12 +29,10 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
end_date: result.end_date, end_date: result.end_date,
}), }),
end_point: result.end_point, end_point: result.end_point,
area: {
type: result.bound_type,
coordinates: area,
},
category_id: result.category_id, category_id: result.category_id,
explanation: result.explanation, explanation: result.explanation,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
...(result.category_id == 3 ...(result.category_id == 3
? { ? {
road_observed_id: result.road_observed_id, road_observed_id: result.road_observed_id,
@@ -64,17 +55,21 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
<CreateForm <CreateForm
defaultValues={{ defaultValues={{
explanation: "", explanation: "",
item_id: "",
sub_item_id: "",
category_id: "", category_id: "",
bound: null,
rahdaran: rahdaran.filter((r) => !r.is_driver), rahdaran: rahdaran.filter((r) => !r.is_driver),
bound_type: "polyline", bound_type: "polyline",
type: "", type: "",
end_km: "",
end_date: "", end_date: "",
end_time: null, end_time: null,
end_point: "", end_point: "",
region: "", region: "",
machine: machine, machine: machine,
driver: rahdaran.find((r) => r.is_driver), driver: row.original.driver_id,
area: null,
locations: [],
}} }}
oldBound={oldBound} oldBound={oldBound}
submitForm={submitForm} submitForm={submitForm}

View File

@@ -3,61 +3,77 @@ import useRequest from "@/lib/hooks/useRequest";
import DialogLoading from "@/core/components/DialogLoading"; import DialogLoading from "@/core/components/DialogLoading";
import CreateForm from "../../../Actions/Create/Form"; import CreateForm from "../../../Actions/Create/Form";
import moment from "jalali-moment"; import moment from "jalali-moment";
import L from "leaflet"; import polyline from "@mapbox/polyline";
import { GET_RAHDARAN_BY_ID, UPDATE_REQUEST_MISSION } from "@/core/utils/routes"; import { GET_RAHDARAN_BY_ID, UPDATE_REQUEST_MISSION } from "@/core/utils/routes";
const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => { const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
const latLngs = useMemo(() => {
return row.original.area.coordinates.map(([lng, lat]) => [lat, lng]);
}, [row.original.area.coordinates]);
const bound = useMemo(
() => (row.original.area.type === "polygon" ? L.polygon(latLngs) : L.polyline(latLngs)),
[row.original.area.type, latLngs]
);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [rahdaran, setRahdaran] = useState(null); const [rahdaran, setRahdaran] = useState(null);
const [rahdaranLoading, setRahdaranLoading] = useState(true); const [rahdaranLoading, setRahdaranLoading] = useState(true);
const requestServer = useRequest(); const requestServer = useRequest();
const locations = useMemo(() => {
if (!row.original.encoded_route) return [];
const decoded = polyline.decode(row.original.encoded_route, 6);
if (decoded.length === 0) return [];
return [
{
lat: decoded[0][0],
lon: decoded[0][1],
},
{
lat: decoded[decoded.length - 1][0],
lon: decoded[decoded.length - 1][1],
},
];
}, [row.original.encoded_route]);
useEffect(() => { useEffect(() => {
setRahdaranLoading(true); setRahdaranLoading(true);
requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get") requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get")
.then((response) => { .then((response) => {
setRahdaran(response.data.data); setRahdaran(response.data.data);
setRahdaranLoading(false);
}) })
.catch((e) => { .catch(() => {})
.finally(() => {
setRahdaranLoading(false); setRahdaranLoading(false);
}); });
}, [row.original.id]); }, [row.original.id]);
const submitForm = async (result) => { const submitForm = async (result) => {
setSubmitting(true); setSubmitting(true);
const latlngs = result.bound.getLatLngs();
const normalized = result.bound_type === "polygon" ? latlngs : [latlngs];
const area = normalized.map((ring) => ring.map(({ lat, lng }) => [lat, lng]));
await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", { await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", {
data: { data: {
explanation: result.explanation, explanation: result.explanation,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
category_id: result.category_id, category_id: result.category_id,
...(result.category_id == 3 ...(result.category_id == 3
? { ? {
road_observed_id: result.road_observed_id, road_observed_id: result.road_observed_id,
} }
: {}), : {}),
area: {
type: result.bound_type, encoded_route: result.area,
coordinates: area[0],
}, ...(result.rahdaran.length !== 0
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}), ? {
rahdaran: result.rahdaran.map((r) => r.id),
}
: {}),
requested_machines: result.requested_machines, requested_machines: result.requested_machines,
type: result.type, type: result.type,
zone: result.region, zone: result.region,
end_point: result.end_point, end_point: result.end_point,
...(result.type == 1 ...(result.type == 1
? { ? {
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`, start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
@@ -71,46 +87,53 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
hasSidebarUpdate: true, hasSidebarUpdate: true,
notificationSuccess: true, notificationSuccess: true,
}) })
.then((response) => { .then(() => {
mutate(); mutate();
setOpenEditDialog(false); setOpenEditDialog(false);
}) })
.catch((error) => {}) .catch(() => {})
.finally(() => { .finally(() => {
setSubmitting(false); setSubmitting(false);
}); });
}; };
return ( return rahdaranLoading ? (
<> <DialogLoading />
{rahdaranLoading ? ( ) : (
<DialogLoading /> <CreateForm
) : ( defaultValues={{
<CreateForm explanation: row.original.explanation,
defaultValues={{ item_id: row.original.item_id,
explanation: row.original.explanation, sub_item_id: row.original.sub_item_id,
category_id: row.original.category_id, category_id: row.original.category_id,
road_observed_id: row.original.category_id == 3 ? row.original.road_observed[0].id : "",
rahdaran: rahdaran.filter((r) => !r.isDriver), road_observed_id: row.original.category_id == 3 ? (row.original.road_observed?.[0]?.id ?? "") : "",
requested_machines: row.original.requested_machines,
bound: bound, rahdaran: rahdaran.filter((r) => !r.isDriver),
bound_type: row.original.area.type,
type: row.original.type, requested_machines: row.original.requested_machines,
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
start_time: moment(row.original.start_date).toDate(), area: row.original.encoded_route,
end_date: moment(row.original.end_date).format("YYYY-MM-DD"), locations,
end_time: moment(row.original.end_date).toDate(),
end_point: row.original.end_point, type: row.original.type,
region: row.original.zone,
}} start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
submitForm={submitForm} start_time: moment(row.original.start_date).toDate(),
submitting={submitting}
open={openEditDialog} end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
setOpen={setOpenEditDialog} end_time: moment(row.original.end_date).toDate(),
mutate={mutate}
/> end_point: row.original.end_point,
)} region: row.original.zone,
</> }}
submitForm={submitForm}
submitting={submitting}
open={openEditDialog}
setOpen={setOpenEditDialog}
mutate={mutate}
/>
); );
}; };
export default EditController; export default EditController;

View File

@@ -7,7 +7,7 @@ const Toolbar = ({ table, filterData, mutate }) => {
return ( return (
<Stack direction={"row"} spacing={1}> <Stack direction={"row"} spacing={1}>
<Create mutate={mutate} /> <Create mutate={mutate} />
<OperatorViolations /> <OperatorViolations mutate={mutate} />
{/*<CreateWithoutProcess mutate={mutate} />*/} {/*<CreateWithoutProcess mutate={mutate} />*/}
</Stack> </Stack>
); );

View File

@@ -0,0 +1,72 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
import RowActions from "./RowActions";
const DriverReportsList = ({ data, specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "driver_id",
header: "کد یکتا",
id: "driver_id",
enableColumnFilter: false,
enableSorting: false,
datatype: "numeric",
grow: false,
size: 50,
},
{
accessorKey: "driver_name",
header: "نام راننده",
id: "driver_name",
enableColumnFilter: false,
enableSorting: false,
datatype: "text",
grow: false,
size: 50,
},
{
accessorKey: "func",
header: "کارکرد",
id: "func",
enableColumnFilter: false,
enableSorting: false,
datatype: "number",
grow: false,
size: 50,
},
{
accessorKey: "missions",
header: "تعداد ماموریت",
id: "missions",
enableColumnFilter: false,
enableSorting: false,
datatype: "number",
grow: false,
size: 50,
},
];
}, []);
const cleanedData = specialFilter.filter((item) => !(item.id === "province_id" && item.value === "-1"));
const RowActionsWithRowData = (props) => <RowActions specialFilter={cleanedData} {...props} />;
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
TableToolbar={Toolbar}
columns={columns}
data={data.data}
page_name={"driverReport"}
table_name={"driverReportList"}
enablePagination={false}
specialFilter={specialFilter}
enableRowActions
RowActions={RowActionsWithRowData}
/>
</Box>
);
};
export default DriverReportsList;

View File

@@ -0,0 +1,77 @@
"use client";
import {
EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_DRIVER_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { useTheme } from "@emotion/react";
import DescriptionIcon from "@mui/icons-material/Description";
import { Button, CircularProgress, IconButton, useMediaQuery } from "@mui/material";
import FileSaver from "file-saver";
import moment from "jalali-moment";
import { useMemo, useState } from "react";
const PrintExcel = ({ table, filterData }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [loading, setLoading] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const activeFilters = Object.entries(filterData).reduce((acc, [key, filter]) => {
if (filter.value) {
acc.push({ id: filter.id, value: filter.value });
}
return acc;
}, []);
const filterParams = useMemo(() => {
const params = new URLSearchParams();
activeFilters.length > 0 &&
activeFilters.map((filter, index) => {
params.set(`${filter.id}`, filter.value);
});
return params;
}, [activeFilters]);
const clickHandler = () => {
setLoading(true);
const CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl =
CountryOrProvince.value === "-1"
? EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_DRIVER_PROVINCE_ACTIVITY_REPORT;
requestServer(`${requestUrl}?${filterParams}`, "get", {
requestOptions: { responseType: "blob" },
})
.then((response) => {
const filename = `خروجی کارتابل گزارشات راننده تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
FileSaver.saveAs(response.data, filename);
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
return (
<>
{isMobile ? (
<IconButton aria-label="خروجی اکسل" color="success" onClick={clickHandler}>
<DescriptionIcon sx={{ fontSize: "25px" }} />
</IconButton>
) : (
<Button
color="success"
variant="contained"
size="small"
disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
onClick={clickHandler}
>
خروجی اکسل
</Button>
)}
</>
);
};
export default PrintExcel;

View File

@@ -0,0 +1,107 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_DRIVER_DETAILS } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
const DriverList = ({ row, specialFilter }) => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 80,
},
{
accessorKey: "province_name",
header: "استان",
id: "province_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "city_name",
header: "شهر",
id: "city_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "station_name",
header: "راهدارخانه",
id: "station_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "driver_name",
header: "راننده",
id: "driver_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "km",
header: "کیلومتر شروع",
id: "km",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "end_km",
header: "کیلومتر پایان",
id: "end_km",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
[]
);
return (
<>
<Box sx={{ mt: 3 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
specialFilter={specialFilter}
sorting={[{ id: "id", desc: true }]}
table_url={`${GET_DRIVER_DETAILS}/${row.driver_id}`}
page_name={"driverListReport"}
table_name={"driverList"}
positionActionsColumn={"first"}
/>
</Box>
</>
);
};
export default DriverList;

View File

@@ -0,0 +1,27 @@
import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import { Stack } from "@mui/system";
import DriverList from "./DriverDetailsDialog/DriverList";
const DriverDetailsTable = ({ row, setOpenDriverDetails, specialFilter }) => {
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack spacing={3}>
<Stack spacing={2}>
<DriverList specialFilter={specialFilter} row={row} />
</Stack>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{ display: "flex", justifyContent: "space-between" }}>
<Stack direction={"row"} spacing={1}>
<Button onClick={() => setOpenDriverDetails(false)} variant="outlined" color="primary">
بستن
</Button>
</Stack>
</DialogActions>
</>
);
};
export default DriverDetailsTable;

View File

@@ -0,0 +1,48 @@
import { Close, RemoveRedEye } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import DriverDetailsTable from "./Form";
const DriverDetails = ({ row, specialFilter }) => {
const [openDriverDetails, setOpenDriverDetails] = useState(false);
return (
<>
<Tooltip title="ریزگزارشات" arrow placement="right">
<IconButton
color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => {
setOpenDriverDetails(true);
}}
>
<RemoveRedEye />
</IconButton>
</Tooltip>
<Dialog open={openDriverDetails} fullWidth maxWidth="md">
<IconButton
aria-label="close"
onClick={() => setOpenDriverDetails(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
<DialogTitle>ریز گزارشات راننده</DialogTitle>
{openDriverDetails && (
<DriverDetailsTable
specialFilter={specialFilter}
row={row.original}
setOpenDriverDetails={setOpenDriverDetails}
/>
)}
</Dialog>
</>
);
};
export default DriverDetails;

View File

@@ -0,0 +1,46 @@
import { useEffect, useRef } from "react";
import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet";
import polyline from "@mapbox/polyline";
import SourceIcon from "@/assets/images/source-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg";
const ShowRoute = ({ area }) => {
const map = useMap();
const defaultIconSize = [35, 35];
const createCustomIcon = (size, iconUrl) => {
return L.icon({
iconUrl: iconUrl,
iconSize: size,
iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]],
});
};
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
if (!area) return null;
const coords = polyline.decode(area, 6);
if (coords.length === 0) return null;
const sourcePosition = coords[0];
const destPosition = coords[coords.length - 1];
return (
<>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
<Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} />
</>
);
};
export default ShowRoute;

View File

@@ -0,0 +1,57 @@
import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import MapIcon from "@mui/icons-material/Map";
import MapLayer from "@/core/components/MapLayer";
import ShowRoute from "./ShowRoute";
const RouteDetail = ({ encoded_route }) => {
const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false);
return (
<>
<Tooltip title="مسیر انتخابی">
<IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}>
<MapIcon />
</IconButton>
</Tooltip>
<Dialog
fullWidth
open={openRouteDetailDialog}
onClose={() => setOpenRouteDetailDialog(false)}
PaperProps={{
sx: {
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
},
}}
dir="rtl"
maxWidth={"sm"}
>
<DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle>
<Box sx={{ mx: 1, borderRadius: "5px" }}>
<Box
sx={{
width: "100%",
height: "300px",
border: "1px solid #e1e1e1",
borderRadius: "5px",
}}
>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={encoded_route} />
</MapLayer>
</Box>
</Box>
<DialogActions>
<Button
onClick={() => setOpenRouteDetailDialog(false)}
variant="outlined"
color="secondary"
autoFocus
>
بستن
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default RouteDetail;

View File

@@ -0,0 +1,6 @@
import DriverDetails from "./DriverDetails";
const RowActions = ({ row, specialFilter }) => {
return <DriverDetails specialFilter={specialFilter} row={row} />;
};
export default RowActions;

View File

@@ -0,0 +1,27 @@
import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment";
import { Controller } from "react-hook-form";
const FromDateController = ({ control }) => {
return (
<Controller
name="from_date"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker
name="from_date"
error={!!error}
value={value}
label={"تاریخ شروع (از تاریخ)"}
placeholder={"از تاریخ"}
setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form
}}
helperText={error ? error.message : null}
/>
)}
/>
);
};
export default FromDateController;

View File

@@ -0,0 +1,44 @@
import SearchIcon from "@mui/icons-material/Search";
import { Button, Grid } from "@mui/material";
import { Controller } from "react-hook-form";
import FromDateController from "./FromDateController";
import SelectProvince from "./SelectProvince";
import ToDateController from "./ToDateController";
const SearchReportField = ({ control, hasProvincesPermission }) => {
return (
<Grid container spacing={2} sx={{ py: 2 }}>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<FromDateController control={control} />
</Grid>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<ToDateController control={control} />
</Grid>
{hasProvincesPermission && (
<Grid item xs={12} md={3}>
<SelectProvince control={control} />
</Grid>
)}
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<Controller
name="submitButton"
control={control}
render={({ field: { value, onChange }, formState: { isSubmitting } }) => (
<Button
variant="contained"
size="medium"
type="submit"
disabled={isSubmitting}
endIcon={<SearchIcon />}
fullWidth
>
{isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"}
</Button>
)}
/>
</Grid>
</Grid>
);
};
export default SearchReportField;

View File

@@ -0,0 +1,45 @@
import useProvinces from "@/lib/hooks/useProvince";
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material";
import { Controller } from "react-hook-form";
const SelectProvince = ({ control }) => {
const { provinces, loadingProvinces, errorProvinces } = useProvinces();
return (
<Controller
name="province_id"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl error={!!error} size="small" fullWidth>
<InputLabel id="demo-error-label">استان</InputLabel>
<Select
{...field}
labelId="demo-error-label"
id="province_id"
value={loadingProvinces ? "loading" : field.value || ""}
label="استان"
onChange={field.onChange}
>
{loadingProvinces ? (
<MenuItem value={"loading"}>در حال بارگذاری...</MenuItem>
) : errorProvinces ? (
<MenuItem value={"error"}>خطا در بارگذاری</MenuItem>
) : (
[
<MenuItem key="-1" value={"-1"}>
کل کشور
</MenuItem>,
...provinces.map((province) => (
<MenuItem key={province.id} value={province.id}>
{province.name_fa}
</MenuItem>
)),
]
)}
</Select>
{error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl>
)}
/>
);
};
export default SelectProvince;

View File

@@ -0,0 +1,30 @@
import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment";
import { Controller, useWatch } from "react-hook-form";
const ToDateController = ({ control }) => {
const minDate = useWatch({ control, name: "from_date" });
return (
<Controller
name="date_to"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker
name="date_to"
minDate={minDate}
maxDate={new Date()}
error={!!error}
value={value}
label={"تاریخ پایان (تا تاریخ)"}
placeholder={"تا تاریخ"}
setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form
}}
helperText={error ? error.message : null}
/>
)}
/>
);
};
export default ToDateController;

View File

@@ -0,0 +1,13 @@
import StyledForm from "@/core/components/StyledForm";
import SearchReportField from "./SearchReportField";
const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => {
return (
<>
<StyledForm onSubmit={handleSubmit(onSearchSubmit)}>
<SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} />
</StyledForm>
</>
);
};
export default SearchReportList;

View File

@@ -0,0 +1,11 @@
import { Box } from "@mui/material";
import PrintExcel from "./ExcelPrint";
const Toolbar = ({ table, filterData, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1 }}>
<PrintExcel table={table} filterData={filterData} />
</Box>
);
};
export default Toolbar;

View File

@@ -0,0 +1,107 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { GET_DRIVER_COUNTRY_ACTIVITY_REPORT, GET_DRIVER_PROVINCE_ACTIVITY_REPORT } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import { usePermissions } from "@/lib/hooks/usePermissions";
import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup";
import { LinearProgress, Stack } from "@mui/material";
import moment from "jalali-moment";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import DriverReportsList from "./DriverReportsList";
import SearchReportList from "./Search";
const DriverReportsPage = () => {
const [data, setData] = useState(null);
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const requestServer = useRequest();
const hasProvincesPermission = userPermissions?.includes("mission-report-country");
const defaultValues = {
from_date: moment(new Date()).format("YYYY-MM-DD"),
date_to: moment(new Date()).format("YYYY-MM-DD"),
province_id: user.province_id || "-1",
};
const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${defaultValues.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
];
const [specialFilter, setSpecialFilter] = useState(defaultFilter);
const onSearchSubmit = async (data) => {
const params = new URLSearchParams();
params.set("from_date", `${data.from_date} 00:00:00`);
params.set("date_to", `${data.date_to} 23:59:59`);
setSpecialFilter([
{ value: `${data.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${data.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${data.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
]);
if (data.province_id === "-1") {
try {
const response = await requestServer(`${GET_DRIVER_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
} else {
try {
params.set("province_id", `${data.province_id}`);
const response = await requestServer(`${GET_DRIVER_PROVINCE_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
}
};
useEffect(() => {
if (!userPermissions) return;
onSearchSubmit(defaultValues);
}, [userPermissions]);
const validationSchema = yup.object().shape({
from_date: yup
.string()
.required("تاریخ شروع الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ شروع الزامی است!")
.test("is-valid-date", "تاریخ شروع معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
date_to: yup
.string()
.required("تاریخ پایان الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ پایان الزامی است!")
.test("is-valid-date", "تاریخ پایان معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
province_id: yup.string().nullable(),
});
const { control, handleSubmit } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
});
return (
<Stack spacing={1}>
<PageTitle title={"کارتابل گزارش راننده"} />
<SearchReportList
control={control}
hasProvincesPermission={hasProvincesPermission}
handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit}
/>
{!data ? <LinearProgress /> : <DriverReportsList specialFilter={specialFilter} data={data} />}
</Stack>
);
};
export default DriverReportsPage;

View File

@@ -1,77 +1,77 @@
"use client"; "use client";
import { import {
EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT, EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT, EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes"; } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest"; import useRequest from "@/lib/hooks/useRequest";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import DescriptionIcon from "@mui/icons-material/Description"; import DescriptionIcon from "@mui/icons-material/Description";
import { Button, CircularProgress, IconButton, useMediaQuery } from "@mui/material"; import { Button, CircularProgress, IconButton, useMediaQuery } from "@mui/material";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
const PrintExcel = ({ table, filterData }) => { const PrintExcel = ({ table, filterData }) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const activeFilters = Object.entries(filterData).reduce((acc, [key, filter]) => { const activeFilters = Object.entries(filterData).reduce((acc, [key, filter]) => {
if (filter.value) { if (filter.value) {
acc.push({ id: filter.id, value: filter.value }); acc.push({ id: filter.id, value: filter.value });
} }
return acc; return acc;
}, []); }, []);
const filterParams = useMemo(() => { const filterParams = useMemo(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
activeFilters.length > 0 && activeFilters.length > 0 &&
activeFilters.map((filter, index) => { activeFilters.map((filter, index) => {
params.set(`${filter.id}`, filter.value); params.set(`${filter.id}`, filter.value);
}); });
return params; return params;
}, [activeFilters]); }, [activeFilters]);
const clickHandler = () => { const clickHandler = () => {
setLoading(true); setLoading(true);
const CountryOrProvince = activeFilters.find((item) => item.id === "province_id"); const CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl = const requestUrl =
CountryOrProvince.value === "-1" CountryOrProvince.value === "-1"
? EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT ? EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT; : EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT;
requestServer(`${requestUrl}?${filterParams}`, "get", { requestServer(`${requestUrl}?${filterParams}`, "get", {
requestOptions: { responseType: "blob" }, requestOptions: { responseType: "blob" },
}) })
.then((response) => { .then((response) => {
const filename = `خروجی کارتابل گزارشات گشت راهداری تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`; const filename = `خروجی کارتابل گزارشات گشت راهداری تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
FileSaver.saveAs(response.data, filename); FileSaver.saveAs(response.data, filename);
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
}; };
return ( return (
<> <>
{isMobile ? ( {isMobile ? (
<IconButton aria-label="خروجی اکسل" color="success" onClick={clickHandler}> <IconButton aria-label="خروجی اکسل" color="success" onClick={clickHandler}>
<DescriptionIcon sx={{ fontSize: "25px" }} /> <DescriptionIcon sx={{ fontSize: "25px" }} />
</IconButton> </IconButton>
) : ( ) : (
<Button <Button
color="success" color="success"
variant="contained" variant="contained"
size="small" size="small"
disabled={loading} disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }} sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />} startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
onClick={clickHandler} onClick={clickHandler}
> >
خروجی اکسل خروجی اکسل
</Button> </Button>
)} )}
</> </>
); );
}; };
export default PrintExcel; export default PrintExcel;

View File

@@ -1,81 +1,82 @@
"use client"; "use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth"; import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useMemo } from "react"; import { useMemo } from "react";
import Toolbar from "./Toolbar"; import Toolbar from "./Toolbar";
import RowActions from "./RowActions"; import RowActions from "./RowActions";
const MachinesReportsList = ({ data, specialFilter }) => { const MachinesReportsList = ({ data, specialFilter }) => {
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {
accessorKey: "machine_id", accessorKey: "machine_id",
header: "کد یکتا", header: "کد یکتا",
id: "machine_id", id: "machine_id",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "numeric", datatype: "numeric",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "car_name", accessorKey: "car_name",
header: "مدل ماشین", header: "مدل ماشین",
id: "car_name", id: "car_name",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "text", datatype: "text",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "machine_code", accessorKey: "machine_code",
header: "کد ماشین", header: "کد ماشین",
id: "machine_code", id: "machine_code",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "text", datatype: "text",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "func", accessorKey: "func",
header: "کارکرد", header: "کارکرد",
id: "func", id: "func",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "number", datatype: "number",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "missions", accessorKey: "missions",
header: "تعداد ماموریت", header: "تعداد ماموریت",
id: "missions", id: "missions",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "number", datatype: "number",
grow: false, grow: false,
size: 50, size: 50,
}, },
]; ];
}, []); }, []);
const cleanedData = specialFilter.filter((item) => !(item.id === "province_id" && item.value === "-1"));
return ( const RowActionsWithRowData = (props) => <RowActions specialFilter={cleanedData} {...props} />;
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}> return (
<DataTableWithAuth <Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
TableToolbar={Toolbar} <DataTableWithAuth
columns={columns} TableToolbar={Toolbar}
data={data.data} columns={columns}
page_name={"roadMissionReport"} data={data.data}
table_name={"roadMissionReportList"} page_name={"roadMissionReport"}
enablePagination={false} table_name={"roadMissionReportList"}
specialFilter={specialFilter} enablePagination={false}
enableRowActions specialFilter={specialFilter}
RowActions={RowActions} enableRowActions
/> RowActions={RowActionsWithRowData}
</Box> />
); </Box>
}; );
};
export default MachinesReportsList;
export default MachinesReportsList;

View File

@@ -1,106 +1,107 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth"; import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_MACHINE_MISSIONS_DETAILS } from "@/core/utils/routes"; import { GET_MACHINE_MISSIONS_DETAILS } from "@/core/utils/routes";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useMemo } from "react"; import { useMemo } from "react";
const MachineMissionsList = ({ row }) => { const MachineMissionsList = ({ row, specialFilter }) => {
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
accessorKey: "id", accessorKey: "id",
header: "کد یکتا", header: "کد یکتا",
id: "id", id: "id",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 80, size: 80,
}, },
{ {
accessorKey: "province_name", accessorKey: "province_name",
header: "استان", header: "استان",
id: "province_name", id: "province_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "city_name", accessorKey: "city_name",
header: "شهر", header: "شهر",
id: "city_name", id: "city_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "station_name", accessorKey: "station_name",
header: "راهدارخانه", header: "راهدارخانه",
id: "station_name", id: "station_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "driver_name", accessorKey: "driver_name",
header: "راننده", header: "راننده",
id: "driver_name", id: "driver_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "km", accessorKey: "km",
header: "کیلومتر شروع", header: "کیلومتر شروع",
id: "km", id: "km",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "end_km", accessorKey: "end_km",
header: "کیلومتر پایان", header: "کیلومتر پایان",
id: "end_km", id: "end_km",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
], ],
[] []
); );
return ( return (
<> <>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<DataTableWithAuth <DataTableWithAuth
need_filter={true} specialFilter={specialFilter}
columns={columns} need_filter={true}
sorting={[{ id: "id", desc: true }]} columns={columns}
table_url={`${GET_MACHINE_MISSIONS_DETAILS}?machine_id=${row.machine_id}`} sorting={[{ id: "id", desc: true }]}
page_name={"roadMissionsTransportation"} table_url={`${GET_MACHINE_MISSIONS_DETAILS}/${row.machine_id}`}
table_name={"roadMissionsTransportationMachinesList"} page_name={"roadMissionsTransportation"}
positionActionsColumn={"first"} table_name={"roadMissionsTransportationMachinesList"}
/> positionActionsColumn={"first"}
</Box> />
</> </Box>
); </>
}; );
export default MachineMissionsList; };
export default MachineMissionsList;

View File

@@ -1,27 +1,27 @@
import { Box, Button, DialogActions, DialogContent } from "@mui/material"; import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import { Stack } from "@mui/system"; import { Stack } from "@mui/system";
import MachineMissionsList from "./MachineMissionsDetailsDialog/MachineMissionsList"; import MachineMissionsList from "./MachineMissionsDetailsDialog/MachineMissionsList";
const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails }) => { const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails, specialFilter }) => {
return ( return (
<> <>
<DialogContent dividers> <DialogContent dividers>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Stack spacing={3}> <Stack spacing={3}>
<Stack spacing={2}> <Stack spacing={2}>
<MachineMissionsList row={row} /> <MachineMissionsList specialFilter={specialFilter} row={row} />
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions sx={{ display: "flex", justifyContent: "space-between" }}> <DialogActions sx={{ display: "flex", justifyContent: "space-between" }}>
<Stack direction={"row"} spacing={1}> <Stack direction={"row"} spacing={1}>
<Button onClick={() => setOpenMachineMissionsDetails(false)} variant="outlined" color="primary"> <Button onClick={() => setOpenMachineMissionsDetails(false)} variant="outlined" color="primary">
بستن بستن
</Button> </Button>
</Stack> </Stack>
</DialogActions> </DialogActions>
</> </>
); );
}; };
export default MachineMissionsDetailsTable; export default MachineMissionsDetailsTable;

View File

@@ -1,47 +1,48 @@
import { Close, RemoveRedEye } from "@mui/icons-material"; import { Close, RemoveRedEye } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material"; import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import MachineMissionsDetailsTable from "./Form"; import MachineMissionsDetailsTable from "./Form";
const MissionDetails = ({ row }) => { const MissionDetails = ({ row, specialFilter }) => {
const [openMachineMissionsDetails, setOpenMachineMissionsDetails] = useState(false); const [openMachineMissionsDetails, setOpenMachineMissionsDetails] = useState(false);
return ( return (
<> <>
<Tooltip title="ریزگزارشات" arrow placement="right"> <Tooltip title="ریزگزارشات" arrow placement="right">
<IconButton <IconButton
color="primary" color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }} sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => { onClick={() => {
setOpenMachineMissionsDetails(true); setOpenMachineMissionsDetails(true);
}} }}
> >
<RemoveRedEye /> <RemoveRedEye />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog open={openMachineMissionsDetails} fullWidth maxWidth="md"> <Dialog open={openMachineMissionsDetails} fullWidth maxWidth="md">
<IconButton <IconButton
aria-label="close" aria-label="close"
onClick={() => setOpenMachineMissionsDetails(false)} onClick={() => setOpenMachineMissionsDetails(false)}
sx={(theme) => ({ sx={(theme) => ({
position: "absolute", position: "absolute",
right: 8, right: 8,
top: 8, top: 8,
zIndex: 50, zIndex: 50,
color: theme.palette.grey[500], color: theme.palette.grey[500],
})} })}
> >
<Close /> <Close />
</IconButton> </IconButton>
<DialogTitle>ریز گزارشات خودرو</DialogTitle> <DialogTitle>ریز گزارشات خودرو</DialogTitle>
{openMachineMissionsDetails && ( {openMachineMissionsDetails && (
<MachineMissionsDetailsTable <MachineMissionsDetailsTable
row={row.original} specialFilter={specialFilter}
setOpenMachineMissionsDetails={setOpenMachineMissionsDetails} row={row.original}
/> setOpenMachineMissionsDetails={setOpenMachineMissionsDetails}
)} />
</Dialog> )}
</> </Dialog>
); </>
}; );
export default MissionDetails; };
export default MissionDetails;

View File

@@ -1,46 +1,46 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Marker, Polyline, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import polyline from "@mapbox/polyline"; import polyline from "@mapbox/polyline";
import SourceIcon from "@/assets/images/source-icon.svg"; import SourceIcon from "@/assets/images/source-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg"; import DestIcon from "@/assets/images/destination-icon.svg";
const ShowRoute = ({ area }) => { const ShowRoute = ({ area }) => {
const map = useMap(); const map = useMap();
const defaultIconSize = [35, 35]; const defaultIconSize = [35, 35];
const createCustomIcon = (size, iconUrl) => { const createCustomIcon = (size, iconUrl) => {
return L.icon({ return L.icon({
iconUrl: iconUrl, iconUrl: iconUrl,
iconSize: size, iconSize: size,
iconAnchor: [size[0] / 2, size[1]], iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]], popupAnchor: [0, -size[1]],
}); });
}; };
useEffect(() => { useEffect(() => {
if (coords.length > 0) { if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 }); map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
} }
}, [area]); }, [area]);
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
if (!area) return null; if (!area) return null;
const coords = polyline.decode(area, 6); const coords = polyline.decode(area, 6);
if (coords.length === 0) return null; if (coords.length === 0) return null;
const sourcePosition = coords[0]; const sourcePosition = coords[0];
const destPosition = coords[coords.length - 1]; const destPosition = coords[coords.length - 1];
return ( return (
<> <>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} /> <Polyline positions={coords} color="#015688" weight={7} opacity={1} />
<Marker position={sourcePosition} icon={sourceIcon} /> <Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} /> <Marker position={destPosition} icon={destIcon} />
</> </>
); );
}; };
export default ShowRoute; export default ShowRoute;

View File

@@ -1,57 +1,57 @@
import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material"; import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import MapIcon from "@mui/icons-material/Map"; import MapIcon from "@mui/icons-material/Map";
import MapLayer from "@/core/components/MapLayer"; import MapLayer from "@/core/components/MapLayer";
import ShowRoute from "./ShowRoute"; import ShowRoute from "./ShowRoute";
const RouteDetail = ({ encoded_route }) => { const RouteDetail = ({ encoded_route }) => {
const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false); const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false);
return ( return (
<> <>
<Tooltip title="مسیر انتخابی"> <Tooltip title="مسیر انتخابی">
<IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}> <IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}>
<MapIcon /> <MapIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog <Dialog
fullWidth fullWidth
open={openRouteDetailDialog} open={openRouteDetailDialog}
onClose={() => setOpenRouteDetailDialog(false)} onClose={() => setOpenRouteDetailDialog(false)}
PaperProps={{ PaperProps={{
sx: { sx: {
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px", boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
}, },
}} }}
dir="rtl" dir="rtl"
maxWidth={"sm"} maxWidth={"sm"}
> >
<DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle> <DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle>
<Box sx={{ mx: 1, borderRadius: "5px" }}> <Box sx={{ mx: 1, borderRadius: "5px" }}>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
height: "300px", height: "300px",
border: "1px solid #e1e1e1", border: "1px solid #e1e1e1",
borderRadius: "5px", borderRadius: "5px",
}} }}
> >
<MapLayer style={{ borderRadius: "4px" }}> <MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={encoded_route} /> <ShowRoute area={encoded_route} />
</MapLayer> </MapLayer>
</Box> </Box>
</Box> </Box>
<DialogActions> <DialogActions>
<Button <Button
onClick={() => setOpenRouteDetailDialog(false)} onClick={() => setOpenRouteDetailDialog(false)}
variant="outlined" variant="outlined"
color="secondary" color="secondary"
autoFocus autoFocus
> >
بستن بستن
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>
); );
}; };
export default RouteDetail; export default RouteDetail;

View File

@@ -1,6 +1,6 @@
import MissionDetails from "./MissionDitails"; import MissionDetails from "./MissionDitails";
const RowActions = ({ row }) => { const RowActions = ({ row, specialFilter }) => {
return <MissionDetails row={row} />; return <MissionDetails specialFilter={specialFilter} row={row} />;
}; };
export default RowActions; export default RowActions;

View File

@@ -1,27 +1,27 @@
import MuiDatePicker from "@/core/components/MuiDatePicker"; import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const FromDateController = ({ control }) => { const FromDateController = ({ control }) => {
return ( return (
<Controller <Controller
name="from_date" name="from_date"
control={control} control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => ( render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker <MuiDatePicker
name="from_date" name="from_date"
error={!!error} error={!!error}
value={value} value={value}
label={"تاریخ شروع (از تاریخ)"} label={"تاریخ شروع (از تاریخ)"}
placeholder={"از تاریخ"} placeholder={"از تاریخ"}
setFieldValue={(name, newValue) => { setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD"); const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form onChange(formattedDate); // Update the field value in react-hook-form
}} }}
helperText={error ? error.message : null} helperText={error ? error.message : null}
/> />
)} )}
/> />
); );
}; };
export default FromDateController; export default FromDateController;

View File

@@ -1,44 +1,44 @@
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { Button, Grid } from "@mui/material"; import { Button, Grid } from "@mui/material";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import FromDateController from "./FromDateController"; import FromDateController from "./FromDateController";
import SelectProvince from "./SelectProvince"; import SelectProvince from "./SelectProvince";
import ToDateController from "./ToDateController"; import ToDateController from "./ToDateController";
const SearchReportField = ({ control, hasProvincesPermission }) => { const SearchReportField = ({ control, hasProvincesPermission }) => {
return ( return (
<Grid container spacing={2} sx={{ py: 2 }}> <Grid container spacing={2} sx={{ py: 2 }}>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<FromDateController control={control} /> <FromDateController control={control} />
</Grid> </Grid>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<ToDateController control={control} /> <ToDateController control={control} />
</Grid> </Grid>
{hasProvincesPermission && ( {hasProvincesPermission && (
<Grid item xs={12} md={3}> <Grid item xs={12} md={3}>
<SelectProvince control={control} /> <SelectProvince control={control} />
</Grid> </Grid>
)} )}
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<Controller <Controller
name="submitButton" name="submitButton"
control={control} control={control}
render={({ field: { value, onChange }, formState: { isSubmitting } }) => ( render={({ field: { value, onChange }, formState: { isSubmitting } }) => (
<Button <Button
variant="contained" variant="contained"
size="medium" size="medium"
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
endIcon={<SearchIcon />} endIcon={<SearchIcon />}
fullWidth fullWidth
> >
{isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"} {isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"}
</Button> </Button>
)} )}
/> />
</Grid> </Grid>
</Grid> </Grid>
); );
}; };
export default SearchReportField; export default SearchReportField;

View File

@@ -1,45 +1,45 @@
import useProvinces from "@/lib/hooks/useProvince"; import useProvinces from "@/lib/hooks/useProvince";
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material"; import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const SelectProvince = ({ control }) => { const SelectProvince = ({ control }) => {
const { provinces, loadingProvinces, errorProvinces } = useProvinces(); const { provinces, loadingProvinces, errorProvinces } = useProvinces();
return ( return (
<Controller <Controller
name="province_id" name="province_id"
control={control} control={control}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl error={!!error} size="small" fullWidth> <FormControl error={!!error} size="small" fullWidth>
<InputLabel id="demo-error-label">استان</InputLabel> <InputLabel id="demo-error-label">استان</InputLabel>
<Select <Select
{...field} {...field}
labelId="demo-error-label" labelId="demo-error-label"
id="province_id" id="province_id"
value={loadingProvinces ? "loading" : field.value || ""} value={loadingProvinces ? "loading" : field.value || ""}
label="استان" label="استان"
onChange={field.onChange} onChange={field.onChange}
> >
{loadingProvinces ? ( {loadingProvinces ? (
<MenuItem value={"loading"}>در حال بارگذاری...</MenuItem> <MenuItem value={"loading"}>در حال بارگذاری...</MenuItem>
) : errorProvinces ? ( ) : errorProvinces ? (
<MenuItem value={"error"}>خطا در بارگذاری</MenuItem> <MenuItem value={"error"}>خطا در بارگذاری</MenuItem>
) : ( ) : (
[ [
<MenuItem key="-1" value={"-1"}> <MenuItem key="-1" value={"-1"}>
کل کشور کل کشور
</MenuItem>, </MenuItem>,
...provinces.map((province) => ( ...provinces.map((province) => (
<MenuItem key={province.id} value={province.id}> <MenuItem key={province.id} value={province.id}>
{province.name_fa} {province.name_fa}
</MenuItem> </MenuItem>
)), )),
] ]
)} )}
</Select> </Select>
{error && <FormHelperText>{error.message}</FormHelperText>} {error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl> </FormControl>
)} )}
/> />
); );
}; };
export default SelectProvince; export default SelectProvince;

View File

@@ -1,30 +1,30 @@
import MuiDatePicker from "@/core/components/MuiDatePicker"; import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { Controller, useWatch } from "react-hook-form"; import { Controller, useWatch } from "react-hook-form";
const ToDateController = ({ control }) => { const ToDateController = ({ control }) => {
const minDate = useWatch({ control, name: "from_date" }); const minDate = useWatch({ control, name: "from_date" });
return ( return (
<Controller <Controller
name="date_to" name="date_to"
control={control} control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => ( render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker <MuiDatePicker
name="date_to" name="date_to"
minDate={minDate} minDate={minDate}
maxDate={new Date()} maxDate={new Date()}
error={!!error} error={!!error}
value={value} value={value}
label={"تاریخ پایان (تا تاریخ)"} label={"تاریخ پایان (تا تاریخ)"}
placeholder={"تا تاریخ"} placeholder={"تا تاریخ"}
setFieldValue={(name, newValue) => { setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD"); const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form onChange(formattedDate); // Update the field value in react-hook-form
}} }}
helperText={error ? error.message : null} helperText={error ? error.message : null}
/> />
)} )}
/> />
); );
}; };
export default ToDateController; export default ToDateController;

View File

@@ -1,13 +1,13 @@
import StyledForm from "@/core/components/StyledForm"; import StyledForm from "@/core/components/StyledForm";
import SearchReportField from "./SearchReportField"; import SearchReportField from "./SearchReportField";
const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => { const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => {
return ( return (
<> <>
<StyledForm onSubmit={handleSubmit(onSearchSubmit)}> <StyledForm onSubmit={handleSubmit(onSearchSubmit)}>
<SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} /> <SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} />
</StyledForm> </StyledForm>
</> </>
); );
}; };
export default SearchReportList; export default SearchReportList;

View File

@@ -1,11 +1,11 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import PrintExcel from "./ExcelPrint"; import PrintExcel from "./ExcelPrint";
const Toolbar = ({ table, filterData, mutate }) => { const Toolbar = ({ table, filterData, mutate }) => {
return ( return (
<Box sx={{ display: "flex", gap: 1 }}> <Box sx={{ display: "flex", gap: 1 }}>
<PrintExcel table={table} filterData={filterData} /> <PrintExcel table={table} filterData={filterData} />
</Box> </Box>
); );
}; };
export default Toolbar; export default Toolbar;

View File

@@ -1,107 +1,107 @@
"use client"; "use client";
import PageTitle from "@/core/components/PageTitle"; import PageTitle from "@/core/components/PageTitle";
import { GET_MACHINES_COUNTRY_ACTIVITY_REPORT, GET_MACHINES_PROVINCE_ACTIVITY_REPORT } from "@/core/utils/routes"; import { GET_MACHINES_COUNTRY_ACTIVITY_REPORT, GET_MACHINES_PROVINCE_ACTIVITY_REPORT } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth"; import { useAuth } from "@/lib/contexts/auth";
import { usePermissions } from "@/lib/hooks/usePermissions"; import { usePermissions } from "@/lib/hooks/usePermissions";
import useRequest from "@/lib/hooks/useRequest"; import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { LinearProgress, Stack } from "@mui/material"; import { LinearProgress, Stack } from "@mui/material";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import MachinesReportsList from "./MachinesReportsList"; import MachinesReportsList from "./MachinesReportsList";
import SearchReportList from "./Search"; import SearchReportList from "./Search";
const MachinesReportsPage = () => { const MachinesReportsPage = () => {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const { data: userPermissions } = usePermissions(); const { data: userPermissions } = usePermissions();
const { user } = useAuth(); const { user } = useAuth();
const requestServer = useRequest(); const requestServer = useRequest();
const hasProvincesPermission = userPermissions?.includes("mission-report-country"); const hasProvincesPermission = userPermissions?.includes("mission-report-country");
const defaultValues = { const defaultValues = {
from_date: moment(new Date()).format("YYYY-MM-DD"), from_date: moment(new Date()).format("YYYY-MM-DD"),
date_to: moment(new Date()).format("YYYY-MM-DD"), date_to: moment(new Date()).format("YYYY-MM-DD"),
province_id: user.province_id || "-1", province_id: user.province_id || "-1",
}; };
const defaultFilter = [ const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" }, { value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date}`, datatype: "date", id: "from_date", fn: "between" }, { value: `${defaultValues.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${defaultValues.date_to}`, datatype: "date", id: "date_to", fn: "between" }, { value: `${defaultValues.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
]; ];
const [specialFilter, setSpecialFilter] = useState(defaultFilter); const [specialFilter, setSpecialFilter] = useState(defaultFilter);
const onSearchSubmit = async (data) => { const onSearchSubmit = async (data) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("from_date", `${data.from_date} 00:00:00`); params.set("from_date", `${data.from_date} 00:00:00`);
params.set("date_to", `${data.date_to} 23:59:59`); params.set("date_to", `${data.date_to} 23:59:59`);
setSpecialFilter([ setSpecialFilter([
{ value: `${data.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" }, { value: `${data.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${data.from_date}`, datatype: "date", id: "from_date", fn: "between" }, { value: `${data.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${data.date_to}`, datatype: "date", id: "date_to", fn: "between" }, { value: `${data.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
]); ]);
if (data.province_id === "-1") { if (data.province_id === "-1") {
try { try {
const response = await requestServer(`${GET_MACHINES_COUNTRY_ACTIVITY_REPORT}?${params}`); const response = await requestServer(`${GET_MACHINES_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data; const result = response.data.data;
setData({ setData({
data: result.activities, data: result.activities,
filters: data, filters: data,
}); });
} catch (e) {} } catch (e) {}
} else { } else {
try { try {
params.set("province_id", `${data.province_id}`); params.set("province_id", `${data.province_id}`);
const response = await requestServer(`${GET_MACHINES_PROVINCE_ACTIVITY_REPORT}?${params}`); const response = await requestServer(`${GET_MACHINES_PROVINCE_ACTIVITY_REPORT}?${params}`);
const result = response.data.data; const result = response.data.data;
setData({ setData({
data: result.activities, data: result.activities,
filters: data, filters: data,
}); });
} catch (e) {} } catch (e) {}
} }
}; };
useEffect(() => { useEffect(() => {
if (!userPermissions) return; if (!userPermissions) return;
onSearchSubmit(defaultValues); onSearchSubmit(defaultValues);
}, [userPermissions]); }, [userPermissions]);
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
from_date: yup from_date: yup
.string() .string()
.required("تاریخ شروع الزامی است!") .required("تاریخ شروع الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ شروع الزامی است!") .matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ شروع الزامی است!")
.test("is-valid-date", "تاریخ شروع معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()), .test("is-valid-date", "تاریخ شروع معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
date_to: yup date_to: yup
.string() .string()
.required("تاریخ پایان الزامی است!") .required("تاریخ پایان الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ پایان الزامی است!") .matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ پایان الزامی است!")
.test("is-valid-date", "تاریخ پایان معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()), .test("is-valid-date", "تاریخ پایان معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
province_id: yup.string().nullable(), province_id: yup.string().nullable(),
}); });
const { control, handleSubmit } = useForm({ const { control, handleSubmit } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
}); });
return ( return (
<Stack spacing={1}> <Stack spacing={1}>
<PageTitle title={"کارتابل گزارش ماشین‌ها"} /> <PageTitle title={"کارتابل گزارش ماشین‌ها"} />
<SearchReportList <SearchReportList
control={control} control={control}
hasProvincesPermission={hasProvincesPermission} hasProvincesPermission={hasProvincesPermission}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit} onSearchSubmit={onSearchSubmit}
/> />
{!data ? <LinearProgress /> : <MachinesReportsList specialFilter={specialFilter} data={data} />} {!data ? <LinearProgress /> : <MachinesReportsList specialFilter={specialFilter} data={data} />}
</Stack> </Stack>
); );
}; };
export default MachinesReportsPage; export default MachinesReportsPage;

View File

@@ -1,61 +1,61 @@
"use client"; "use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth"; import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useMemo } from "react"; import { useMemo } from "react";
import Toolbar from "./Toolbar"; import Toolbar from "./Toolbar";
// import RowActions from "./RowActions"; // import RowActions from "./RowActions";
const MachinesReportsList = ({ data, specialFilter }) => { const MachinesReportsList = ({ data, specialFilter }) => {
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {
accessorKey: "car_type", accessorKey: "car_type",
header: "نوع ماشین", header: "نوع ماشین",
id: "car_type", id: "car_type",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "text", datatype: "text",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "func", accessorKey: "func",
header: "کارکرد", header: "کارکرد",
id: "func", id: "func",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "number", datatype: "number",
grow: false, grow: false,
size: 50, size: 50,
}, },
{ {
accessorKey: "missions", accessorKey: "missions",
header: "تعداد ماموریت", header: "تعداد ماموریت",
id: "missions", id: "missions",
enableColumnFilter: false, enableColumnFilter: false,
enableSorting: false, enableSorting: false,
datatype: "number", datatype: "number",
grow: false, grow: false,
size: 50, size: 50,
}, },
]; ];
}, []); }, []);
return ( return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}> <Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth <DataTableWithAuth
TableToolbar={Toolbar} TableToolbar={Toolbar}
columns={columns} columns={columns}
data={data.data} data={data.data}
page_name={"roadMissionReportMachineTypeReport"} page_name={"roadMissionReportMachineTypeReport"}
table_name={"roadMissionReportMachineTypeReportList"} table_name={"roadMissionReportMachineTypeReportList"}
enablePagination={false} enablePagination={false}
specialFilter={specialFilter} specialFilter={specialFilter}
// enableRowActions // enableRowActions
// RowActions={RowActions} // RowActions={RowActions}
/> />
</Box> </Box>
); );
}; };
export default MachinesReportsList; export default MachinesReportsList;

View File

@@ -1,106 +1,106 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth"; import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_MACHINE_MISSIONS_DETAILS, GET_MACHINE_TYPE_MISSIONS_DETAILS } from "@/core/utils/routes"; import { GET_MACHINE_MISSIONS_DETAILS, GET_MACHINE_TYPE_MISSIONS_DETAILS } from "@/core/utils/routes";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useMemo } from "react"; import { useMemo } from "react";
const MachineMissionsList = ({ row }) => { const MachineMissionsList = ({ row }) => {
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
accessorKey: "id", accessorKey: "id",
header: "کد یکتا", header: "کد یکتا",
id: "id", id: "id",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 80, size: 80,
}, },
{ {
accessorKey: "province_name", accessorKey: "province_name",
header: "استان", header: "استان",
id: "province_name", id: "province_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "city_name", accessorKey: "city_name",
header: "شهر", header: "شهر",
id: "city_name", id: "city_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "station_name", accessorKey: "station_name",
header: "راهدارخانه", header: "راهدارخانه",
id: "station_name", id: "station_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "driver_name", accessorKey: "driver_name",
header: "راننده", header: "راننده",
id: "driver_name", id: "driver_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "km", accessorKey: "km",
header: "کیلومتر شروع", header: "کیلومتر شروع",
id: "km", id: "km",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "end_km", accessorKey: "end_km",
header: "کیلومتر پایان", header: "کیلومتر پایان",
id: "end_km", id: "end_km",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
], ],
[] []
); );
return ( return (
<> <>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<DataTableWithAuth <DataTableWithAuth
need_filter={true} need_filter={true}
columns={columns} columns={columns}
sorting={[{ id: "id", desc: true }]} sorting={[{ id: "id", desc: true }]}
table_url={`${GET_MACHINE_TYPE_MISSIONS_DETAILS}?car_type=${row.car_type}`} table_url={`${GET_MACHINE_TYPE_MISSIONS_DETAILS}?car_type=${row.car_type}`}
page_name={"roadMissionReportMachineType"} page_name={"roadMissionReportMachineType"}
table_name={"roadMissionReportMachineTypeList"} table_name={"roadMissionReportMachineTypeList"}
positionActionsColumn={"first"} positionActionsColumn={"first"}
/> />
</Box> </Box>
</> </>
); );
}; };
export default MachineMissionsList; export default MachineMissionsList;

View File

@@ -1,27 +1,27 @@
import { Box, Button, DialogActions, DialogContent } from "@mui/material"; import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import { Stack } from "@mui/system"; import { Stack } from "@mui/system";
import MachineMissionsList from "./MachineMissionsDetailsDialog/MachineMissionsList"; import MachineMissionsList from "./MachineMissionsDetailsDialog/MachineMissionsList";
const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails }) => { const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails }) => {
return ( return (
<> <>
<DialogContent dividers> <DialogContent dividers>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Stack spacing={3}> <Stack spacing={3}>
<Stack spacing={2}> <Stack spacing={2}>
<MachineMissionsList row={row} /> <MachineMissionsList row={row} />
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions sx={{ display: "flex", justifyContent: "space-between" }}> <DialogActions sx={{ display: "flex", justifyContent: "space-between" }}>
<Stack direction={"row"} spacing={1}> <Stack direction={"row"} spacing={1}>
<Button onClick={() => setOpenMachineMissionsDetails(false)} variant="outlined" color="primary"> <Button onClick={() => setOpenMachineMissionsDetails(false)} variant="outlined" color="primary">
بستن بستن
</Button> </Button>
</Stack> </Stack>
</DialogActions> </DialogActions>
</> </>
); );
}; };
export default MachineMissionsDetailsTable; export default MachineMissionsDetailsTable;

View File

@@ -1,47 +1,47 @@
import { Close, RemoveRedEye } from "@mui/icons-material"; import { Close, RemoveRedEye } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material"; import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import MachineMissionsDetailsTable from "./Form"; import MachineMissionsDetailsTable from "./Form";
const MissionDetails = ({ row }) => { const MissionDetails = ({ row }) => {
const [openMachineMissionsDetails, setOpenMachineMissionsDetails] = useState(false); const [openMachineMissionsDetails, setOpenMachineMissionsDetails] = useState(false);
return ( return (
<> <>
<Tooltip title="ریزگزارشات" arrow placement="right"> <Tooltip title="ریزگزارشات" arrow placement="right">
<IconButton <IconButton
color="primary" color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }} sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => { onClick={() => {
setOpenMachineMissionsDetails(true); setOpenMachineMissionsDetails(true);
}} }}
> >
<RemoveRedEye /> <RemoveRedEye />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog open={openMachineMissionsDetails} fullWidth maxWidth="md"> <Dialog open={openMachineMissionsDetails} fullWidth maxWidth="md">
<IconButton <IconButton
aria-label="close" aria-label="close"
onClick={() => setOpenMachineMissionsDetails(false)} onClick={() => setOpenMachineMissionsDetails(false)}
sx={(theme) => ({ sx={(theme) => ({
position: "absolute", position: "absolute",
right: 8, right: 8,
top: 8, top: 8,
zIndex: 50, zIndex: 50,
color: theme.palette.grey[500], color: theme.palette.grey[500],
})} })}
> >
<Close /> <Close />
</IconButton> </IconButton>
<DialogTitle>ریز گزارشات نوع خودرو</DialogTitle> <DialogTitle>ریز گزارشات نوع خودرو</DialogTitle>
{openMachineMissionsDetails && ( {openMachineMissionsDetails && (
<MachineMissionsDetailsTable <MachineMissionsDetailsTable
row={row.original} row={row.original}
setOpenMachineMissionsDetails={setOpenMachineMissionsDetails} setOpenMachineMissionsDetails={setOpenMachineMissionsDetails}
/> />
)} )}
</Dialog> </Dialog>
</> </>
); );
}; };
export default MissionDetails; export default MissionDetails;

View File

@@ -1,46 +1,46 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Marker, Polyline, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import polyline from "@mapbox/polyline"; import polyline from "@mapbox/polyline";
import SourceIcon from "@/assets/images/source-icon.svg"; import SourceIcon from "@/assets/images/source-icon.svg";
import DestIcon from "@/assets/images/destination-icon.svg"; import DestIcon from "@/assets/images/destination-icon.svg";
const ShowRoute = ({ area }) => { const ShowRoute = ({ area }) => {
const map = useMap(); const map = useMap();
const defaultIconSize = [35, 35]; const defaultIconSize = [35, 35];
const createCustomIcon = (size, iconUrl) => { const createCustomIcon = (size, iconUrl) => {
return L.icon({ return L.icon({
iconUrl: iconUrl, iconUrl: iconUrl,
iconSize: size, iconSize: size,
iconAnchor: [size[0] / 2, size[1]], iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]], popupAnchor: [0, -size[1]],
}); });
}; };
useEffect(() => { useEffect(() => {
if (coords.length > 0) { if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 }); map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
} }
}, [area]); }, [area]);
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
if (!area) return null; if (!area) return null;
const coords = polyline.decode(area, 6); const coords = polyline.decode(area, 6);
if (coords.length === 0) return null; if (coords.length === 0) return null;
const sourcePosition = coords[0]; const sourcePosition = coords[0];
const destPosition = coords[coords.length - 1]; const destPosition = coords[coords.length - 1];
return ( return (
<> <>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} /> <Polyline positions={coords} color="#015688" weight={7} opacity={1} />
<Marker position={sourcePosition} icon={sourceIcon} /> <Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} /> <Marker position={destPosition} icon={destIcon} />
</> </>
); );
}; };
export default ShowRoute; export default ShowRoute;

View File

@@ -1,57 +1,57 @@
import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material"; import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import MapIcon from "@mui/icons-material/Map"; import MapIcon from "@mui/icons-material/Map";
import MapLayer from "@/core/components/MapLayer"; import MapLayer from "@/core/components/MapLayer";
import ShowRoute from "./ShowRoute"; import ShowRoute from "./ShowRoute";
const RouteDetail = ({ encoded_route }) => { const RouteDetail = ({ encoded_route }) => {
const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false); const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false);
return ( return (
<> <>
<Tooltip title="مسیر انتخابی"> <Tooltip title="مسیر انتخابی">
<IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}> <IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}>
<MapIcon /> <MapIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog <Dialog
fullWidth fullWidth
open={openRouteDetailDialog} open={openRouteDetailDialog}
onClose={() => setOpenRouteDetailDialog(false)} onClose={() => setOpenRouteDetailDialog(false)}
PaperProps={{ PaperProps={{
sx: { sx: {
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px", boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
}, },
}} }}
dir="rtl" dir="rtl"
maxWidth={"sm"} maxWidth={"sm"}
> >
<DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle> <DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle>
<Box sx={{ mx: 1, borderRadius: "5px" }}> <Box sx={{ mx: 1, borderRadius: "5px" }}>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
height: "300px", height: "300px",
border: "1px solid #e1e1e1", border: "1px solid #e1e1e1",
borderRadius: "5px", borderRadius: "5px",
}} }}
> >
<MapLayer style={{ borderRadius: "4px" }}> <MapLayer style={{ borderRadius: "4px" }}>
<ShowRoute area={encoded_route} /> <ShowRoute area={encoded_route} />
</MapLayer> </MapLayer>
</Box> </Box>
</Box> </Box>
<DialogActions> <DialogActions>
<Button <Button
onClick={() => setOpenRouteDetailDialog(false)} onClick={() => setOpenRouteDetailDialog(false)}
variant="outlined" variant="outlined"
color="secondary" color="secondary"
autoFocus autoFocus
> >
بستن بستن
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>
); );
}; };
export default RouteDetail; export default RouteDetail;

View File

@@ -1,10 +1,6 @@
// import MissionDetails from "./MissionDitails"; // import MissionDetails from "./MissionDitails";
const RowActions = ({ row }) => { const RowActions = ({ row }) => {
return ( return <>{/*<MissionDetails row={row} />*/}</>;
<> };
{/*<MissionDetails row={row} />*/} export default RowActions;
</>
);
};
export default RowActions;

View File

@@ -1,27 +1,27 @@
import MuiDatePicker from "@/core/components/MuiDatePicker"; import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const FromDateController = ({ control }) => { const FromDateController = ({ control }) => {
return ( return (
<Controller <Controller
name="from_date" name="from_date"
control={control} control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => ( render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker <MuiDatePicker
name="from_date" name="from_date"
error={!!error} error={!!error}
value={value} value={value}
label={"تاریخ شروع (از تاریخ)"} label={"تاریخ شروع (از تاریخ)"}
placeholder={"از تاریخ"} placeholder={"از تاریخ"}
setFieldValue={(name, newValue) => { setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD"); const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form onChange(formattedDate); // Update the field value in react-hook-form
}} }}
helperText={error ? error.message : null} helperText={error ? error.message : null}
/> />
)} )}
/> />
); );
}; };
export default FromDateController; export default FromDateController;

View File

@@ -1,44 +1,44 @@
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { Button, Grid } from "@mui/material"; import { Button, Grid } from "@mui/material";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import FromDateController from "./FromDateController"; import FromDateController from "./FromDateController";
import SelectProvince from "./SelectProvince"; import SelectProvince from "./SelectProvince";
import ToDateController from "./ToDateController"; import ToDateController from "./ToDateController";
const SearchReportField = ({ control, hasProvincesPermission }) => { const SearchReportField = ({ control, hasProvincesPermission }) => {
return ( return (
<Grid container spacing={2} sx={{ py: 2 }}> <Grid container spacing={2} sx={{ py: 2 }}>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<FromDateController control={control} /> <FromDateController control={control} />
</Grid> </Grid>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<ToDateController control={control} /> <ToDateController control={control} />
</Grid> </Grid>
{hasProvincesPermission && ( {hasProvincesPermission && (
<Grid item xs={12} md={3}> <Grid item xs={12} md={3}>
<SelectProvince control={control} /> <SelectProvince control={control} />
</Grid> </Grid>
)} )}
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}> <Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<Controller <Controller
name="submitButton" name="submitButton"
control={control} control={control}
render={({ field: { value, onChange }, formState: { isSubmitting } }) => ( render={({ field: { value, onChange }, formState: { isSubmitting } }) => (
<Button <Button
variant="contained" variant="contained"
size="medium" size="medium"
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
endIcon={<SearchIcon />} endIcon={<SearchIcon />}
fullWidth fullWidth
> >
{isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"} {isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"}
</Button> </Button>
)} )}
/> />
</Grid> </Grid>
</Grid> </Grid>
); );
}; };
export default SearchReportField; export default SearchReportField;

View File

@@ -1,45 +1,45 @@
import useProvinces from "@/lib/hooks/useProvince"; import useProvinces from "@/lib/hooks/useProvince";
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material"; import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const SelectProvince = ({ control }) => { const SelectProvince = ({ control }) => {
const { provinces, loadingProvinces, errorProvinces } = useProvinces(); const { provinces, loadingProvinces, errorProvinces } = useProvinces();
return ( return (
<Controller <Controller
name="province_id" name="province_id"
control={control} control={control}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl error={!!error} size="small" fullWidth> <FormControl error={!!error} size="small" fullWidth>
<InputLabel id="demo-error-label">استان</InputLabel> <InputLabel id="demo-error-label">استان</InputLabel>
<Select <Select
{...field} {...field}
labelId="demo-error-label" labelId="demo-error-label"
id="province_id" id="province_id"
value={loadingProvinces ? "loading" : field.value || ""} value={loadingProvinces ? "loading" : field.value || ""}
label="استان" label="استان"
onChange={field.onChange} onChange={field.onChange}
> >
{loadingProvinces ? ( {loadingProvinces ? (
<MenuItem value={"loading"}>در حال بارگذاری...</MenuItem> <MenuItem value={"loading"}>در حال بارگذاری...</MenuItem>
) : errorProvinces ? ( ) : errorProvinces ? (
<MenuItem value={"error"}>خطا در بارگذاری</MenuItem> <MenuItem value={"error"}>خطا در بارگذاری</MenuItem>
) : ( ) : (
[ [
<MenuItem key="-1" value={"-1"}> <MenuItem key="-1" value={"-1"}>
کل کشور کل کشور
</MenuItem>, </MenuItem>,
...provinces.map((province) => ( ...provinces.map((province) => (
<MenuItem key={province.id} value={province.id}> <MenuItem key={province.id} value={province.id}>
{province.name_fa} {province.name_fa}
</MenuItem> </MenuItem>
)), )),
] ]
)} )}
</Select> </Select>
{error && <FormHelperText>{error.message}</FormHelperText>} {error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl> </FormControl>
)} )}
/> />
); );
}; };
export default SelectProvince; export default SelectProvince;

View File

@@ -1,30 +1,30 @@
import MuiDatePicker from "@/core/components/MuiDatePicker"; import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment"; import moment from "jalali-moment";
import { Controller, useWatch } from "react-hook-form"; import { Controller, useWatch } from "react-hook-form";
const ToDateController = ({ control }) => { const ToDateController = ({ control }) => {
const minDate = useWatch({ control, name: "from_date" }); const minDate = useWatch({ control, name: "from_date" });
return ( return (
<Controller <Controller
name="date_to" name="date_to"
control={control} control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => ( render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker <MuiDatePicker
name="date_to" name="date_to"
minDate={minDate} minDate={minDate}
maxDate={new Date()} maxDate={new Date()}
error={!!error} error={!!error}
value={value} value={value}
label={"تاریخ پایان (تا تاریخ)"} label={"تاریخ پایان (تا تاریخ)"}
placeholder={"تا تاریخ"} placeholder={"تا تاریخ"}
setFieldValue={(name, newValue) => { setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD"); const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form onChange(formattedDate); // Update the field value in react-hook-form
}} }}
helperText={error ? error.message : null} helperText={error ? error.message : null}
/> />
)} )}
/> />
); );
}; };
export default ToDateController; export default ToDateController;

View File

@@ -1,13 +1,13 @@
import StyledForm from "@/core/components/StyledForm"; import StyledForm from "@/core/components/StyledForm";
import SearchReportField from "./SearchReportField"; import SearchReportField from "./SearchReportField";
const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => { const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => {
return ( return (
<> <>
<StyledForm onSubmit={handleSubmit(onSearchSubmit)}> <StyledForm onSubmit={handleSubmit(onSearchSubmit)}>
<SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} /> <SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} />
</StyledForm> </StyledForm>
</> </>
); );
}; };
export default SearchReportList; export default SearchReportList;

View File

@@ -1,11 +1,11 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import PrintExcel from "./ExcelPrint"; import PrintExcel from "./ExcelPrint";
const Toolbar = ({ table, filterData, mutate }) => { const Toolbar = ({ table, filterData, mutate }) => {
return ( return (
<Box sx={{ display: "flex", gap: 1 }}> <Box sx={{ display: "flex", gap: 1 }}>
<PrintExcel table={table} filterData={filterData} /> <PrintExcel table={table} filterData={filterData} />
</Box> </Box>
); );
}; };
export default Toolbar; export default Toolbar;

View File

@@ -78,7 +78,6 @@ const MachinesList = ({ machineType, setMachine, setOpenMachinesDialog }) => {
columns={columns} columns={columns}
specialFilter={[ specialFilter={[
{ value: machineType, datatype: "text", id: "car_type", fn: "equals" }, { value: machineType, datatype: "text", id: "car_type", fn: "equals" },
{ value: 1, datatype: "numeric", id: "status", fn: "equals" },
{ {
value: city_id ? city_id : province_id, value: city_id ? city_id : province_id,
datatype: "numeric", datatype: "numeric",

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Marker, Polyline, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import polyline from "@mapbox/polyline"; import polyline from "@mapbox/polyline";
@@ -18,14 +18,24 @@ const ShowRoute = ({ area }) => {
}); });
}; };
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src); const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src); const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
const [showRoute, setShowRoute] = useState(false);
useEffect(() => {
if (!coords.length) return;
setShowRoute(false);
map.flyToBounds(coords, {
padding: [50, 50],
duration: 1.5,
});
map.once("moveend", () => {
setShowRoute(true);
});
}, [area]);
if (!area) return null; if (!area) return null;
const coords = polyline.decode(area, 6); const coords = polyline.decode(area, 6);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
return ( return (
<> <>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} /> {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
<Marker position={sourcePosition} icon={sourceIcon} /> <Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} /> <Marker position={destPosition} icon={destIcon} />
</> </>

View File

@@ -20,7 +20,7 @@ const ViolationForm = ({ setOpenMachinesDialog, mutate, openMachinesDialog }) =>
data: { data: {
machine_code: result.machinesId.machine_code, machine_code: result.machinesId.machine_code,
km: result.km, km: result.km,
exit_station: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`, exit_time: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
}, },
}) })
.then((response) => { .then((response) => {

View File

@@ -11,14 +11,16 @@ const EditController = ({ rowId, mutate, setOpen, row }) => {
const defaultData = { const defaultData = {
end_date: endDate, end_date: endDate,
end_time: row?.original?.enter_station ? new Date(row.original.enter_station) : null, end_km: "",
end_time: row?.original?.enter_time ? new Date(row.original.enter_time) : null,
}; };
const handleSubmit = async (result) => { const handleSubmit = async (result) => {
const payload = {}; const payload = {};
if (result.end_date && result.end_time) { if (result.end_date && result.end_time) {
payload.enter_station = `${result.end_date} ${moment(result.end_time).format("HH:mm")}`; payload.enter_time = `${result.end_date} ${moment(result.end_time).format("HH:mm")}`;
} }
payload.end_km = result.end_km;
await requestServer(`${EDIT_VIOLATION}/${rowId}`, "post", { await requestServer(`${EDIT_VIOLATION}/${rowId}`, "post", {
data: payload, data: payload,

View File

@@ -1,9 +1,10 @@
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import StyledForm from "@/core/components/StyledForm"; import StyledForm from "@/core/components/StyledForm";
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
import MissionDates from "./MissionDates"; import MissionDates from "./MissionDates";
import * as yup from "yup"; import * as yup from "yup";
import LtrTextField from "@/core/components/LtrTextField";
export const schema = yup export const schema = yup
.object({ .object({
@@ -55,7 +56,7 @@ const EditViolationForm = ({ setOpenMachinesDialog, defaultValues, onSubmitBase
<> <>
<StyledForm onSubmit={handleSubmit(handleOnSubmit)}> <StyledForm onSubmit={handleSubmit(handleOnSubmit)}>
<DialogContent dividers> <DialogContent dividers>
<Box sx={{ flex: 1 }}> <Stack spacing={2} sx={{ flex: 1 }}>
<Stack> <Stack>
<Grid container spacing={2}> <Grid container spacing={2}>
<MissionDates control={control} /> <MissionDates control={control} />
@@ -73,7 +74,28 @@ const EditViolationForm = ({ setOpenMachinesDialog, defaultValues, onSubmitBase
</Box> </Box>
)} )}
</Stack> </Stack>
</Box> <Stack>
<Controller
control={control}
name={"end_km"}
render={({ field, fieldState: { error } }) => (
<LtrTextField
autoComplete="off"
type="tel"
value={field.value}
onChange={(e) => {
if (isNaN(Number(e.target.value))) return;
field.onChange(e.target.value);
}}
size="small"
fullWidth
label="کیلومتر خودرو / ساعت کار"
InputLabelProps={{ shrink: true }}
/>
)}
/>
</Stack>
</Stack>
</DialogContent> </DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}> <DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button <Button

View File

@@ -35,6 +35,7 @@ import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
import SpeedIcon from "@mui/icons-material/Speed"; import SpeedIcon from "@mui/icons-material/Speed";
import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle"; import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle";
import TaxiAlertIcon from "@mui/icons-material/TaxiAlert"; import TaxiAlertIcon from "@mui/icons-material/TaxiAlert";
import AirlineSeatReclineNormalIcon from "@mui/icons-material/AirlineSeatReclineNormal";
export const pageMenu = [ export const pageMenu = [
{ {
@@ -211,6 +212,15 @@ export const pageMenu = [
badges: [{ key: "mission.request_portal" }], badges: [{ key: "mission.request_portal" }],
permissions: ["all"], permissions: ["all"],
}, },
{
id: "driverReports",
label: "راننده",
icon: <AirlineSeatReclineNormalIcon sx={{ width: "inherit", height: "inherit" }} />,
type: "page",
route: "/dashboard/road-missions/reports/driverReports",
badges: [{ key: "mission.request_portal" }],
permissions: ["all"],
},
], ],
}, },
], ],

View File

@@ -33,6 +33,7 @@ import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
import SpeedIcon from "@mui/icons-material/Speed"; import SpeedIcon from "@mui/icons-material/Speed";
import TaxiAlertIcon from "@mui/icons-material/TaxiAlert"; import TaxiAlertIcon from "@mui/icons-material/TaxiAlert";
import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle"; import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle";
import AirlineSeatReclineNormalIcon from "@mui/icons-material/AirlineSeatReclineNormal";
export const pageMenuDev = [ export const pageMenuDev = [
{ {
@@ -209,6 +210,15 @@ export const pageMenuDev = [
badges: [{ key: "mission.request_portal" }], badges: [{ key: "mission.request_portal" }],
permissions: ["all"], permissions: ["all"],
}, },
{
id: "driverReports",
label: "راننده",
icon: <AirlineSeatReclineNormalIcon sx={{ width: "inherit", height: "inherit" }} />,
type: "page",
route: "/dashboard/road-missions/reports/driverReports",
badges: [{ key: "mission.request_portal" }],
permissions: ["all"],
},
], ],
}, },
], ],

View File

@@ -256,9 +256,19 @@ export const EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT =
export const EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT = export const EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machines_activity_excel"; api + "/api/v3/missions/report_machine/province_machines_activity_excel";
export const GET_DRIVER_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machine_drivers_activity";
export const GET_DRIVER_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machine_drivers_activity";
export const EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machine_drivers_activity_excel";
export const EXPORT_GET_DRIVER_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machine_drivers_activity_excel";
export const GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT = export const GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machine_types_activity"; api + "/api/v3/missions/report_machine/country_machine_types_activity";
export const GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/province_machine_types_activity"; export const GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machine_types_activity";
export const EXPORT_GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT = export const EXPORT_GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machine_types_activity_excel"; api + "/api/v3/missions/report_machine/country_machine_types_activity_excel";
export const EXPORT_GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT = export const EXPORT_GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT =
@@ -304,3 +314,4 @@ export const GET_STATIONS_LIST = api + "/api/v3/road_maintenance_station";
export const UPDATE_MACHINE_STATUS_AND_LOCATION = api + "/api/v3/cmms_machines"; export const UPDATE_MACHINE_STATUS_AND_LOCATION = api + "/api/v3/cmms_machines";
export const GET_MACHINE_MISSIONS_DETAILS = api + "/api/v3/missions/report_machine/machine_details_report"; export const GET_MACHINE_MISSIONS_DETAILS = api + "/api/v3/missions/report_machine/machine_details_report";
export const GET_MACHINE_TYPE_MISSIONS_DETAILS = api + "/api/v3/missions/report_machine/machine_type_details_report"; export const GET_MACHINE_TYPE_MISSIONS_DETAILS = api + "/api/v3/missions/report_machine/machine_type_details_report";
export const GET_DRIVER_DETAILS = api + "/api/v3/missions/report_machine/machine_driver_details_report";