33 Commits

Author SHA1 Message Date
2f91d31816 Merge tag 'v1.9.2' into develop
v1.9.2
2026-07-14 15:04:16 +03:30
40437079c9 Merge branch 'release/v1.9.2' 2026-07-14 15:04:06 +03:30
605114e0e4 v1.9.2 2026-07-14 15:03:30 +03:30
0ce0b608a4 Merge pull request 'bugfix/delete_violations' (#14) from bugfix/delete_violations into develop
Reviewed-on: #14
2026-07-14 11:26:11 +00:00
457bef64c4 add deleteviolations 2026-07-14 14:55:30 +03:30
f78a2c3cbb build and format 2026-07-14 14:46:03 +03:30
2c7679e52b add excel report to road missions 2026-07-14 14:44:21 +03:30
42194d2030 Merge branch 'develop' of http://git.witel.ir/RMS/frontend into bugfix/delete_violations 2026-07-14 09:00:46 +03:30
6520dd0365 Merge pull request 'feature/missions_table_updates' (#13) from feature/missions_table_updates into develop
Reviewed-on: #13
2026-07-14 05:30:09 +00:00
6cf30a591c add delete violation and base report page 2026-07-14 08:59:49 +03:30
1680cb44b7 added enter/exit columns to operator violations 2026-07-12 11:39:39 +03:30
51afdd47a0 added province and city filter to machine missions 2026-07-12 11:11:08 +03:30
f0d9a17e01 Merge tag 'v1.9.1' into develop
v1.9.1
2026-06-22 15:21:40 +03:30
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
62 changed files with 2248 additions and 630 deletions

View File

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

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;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
bottom: 12,
left: 12,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
backgroundColor: "rgba(255,255,255,0.95)",
backdropFilter: "blur(6px)",
border: "1px solid #ececec",
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
px: 1.25,
py: 0.75,
width: 230,
maxWidth: "70vw",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
<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)}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
))}
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
<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>
);

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 L from "leaflet";
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 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);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
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={destPosition} icon={destIcon} />
</>
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
};
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 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);
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
);
const isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
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) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
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() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
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() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
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));
}
},
});
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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);
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100);
} 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 = [
{ lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng },
];
try {
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
}
};
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);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition, waypoints);
}
}
};
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
setDestPosition(clickedPosition);
setIsDest(true);
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 = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
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) => {
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
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()}
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
icon={prevDestIcon}
/>
)}
{alternateCoords.map((coords, index) => (
<Polyline
key={`alternate-${index}-${selectedRoute?.index === index}`}

View File

@@ -6,107 +6,107 @@ import { Controller, useWatch } from "react-hook-form";
const MissionDates = ({ control }) => {
const type = useWatch({ control, name: "type" });
const start_date = useWatch({ control, name: "start_date" });
const end_date = useWatch({ control, name: "end_date" });
const start_time = useWatch({ control, name: "start_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 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 (
type && (
<>
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"start_date"}
render={({ field, fieldState: { error } }) => {
return (
<MuiDatePicker
name="start_date"
error={error}
value={field.value}
disableFuture={false}
minDate={now}
maxDate={end_date ?? null}
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
label={"تاریخ شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
name="start_date"
render={({ field, fieldState: { error } }) => (
<MuiDatePicker
name="start_date"
value={field.value}
error={error}
disableFuture={false}
minDate={now}
maxDate={end_date ?? null}
label="تاریخ شروع ماموریت"
placeholder="تاریخ شروع ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
{type == 1 && (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"start_time"}
render={({ field, fieldState: { error } }) => {
return (
<MuiTimePicker
name="start_time"
error={error}
value={field.value}
minTime={
start_date && new Date(start_date).toDateString() === now.toDateString()
? now
: null
}
maxTime={endDateTime ?? null}
placeholder={"زمان شروع ماموریت را وارد کنید"}
label={"زمان شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
name="start_time"
render={({ field, fieldState: { error } }) => (
<MuiTimePicker
name="start_time"
value={field.value}
error={error}
minTime={isToday ? now : null}
maxTime={isSameDay && endDateTime ? endDateTime : null}
label="زمان شروع ماموریت"
placeholder="زمان شروع ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
)}
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"end_date"}
render={({ field, fieldState: { error } }) => {
return (
<MuiDatePicker
name="end_date"
error={error}
value={field.value}
disableFuture={false}
minDate={start_date ? start_date : now}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
name="end_date"
render={({ field, fieldState: { error } }) => (
<MuiDatePicker
name="end_date"
value={field.value}
error={error}
disableFuture={false}
minDate={start_date || now}
label="تاریخ پایان ماموریت"
placeholder="تاریخ پایان ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
{type == 1 && (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"end_time"}
render={({ field, fieldState: { error } }) => {
return (
<MuiTimePicker
name="end_time"
error={error}
value={field.value}
minTime={startDateTime ?? null}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
name="end_time"
render={({ field, fieldState: { error } }) => (
<MuiTimePicker
name="end_time"
value={field.value}
error={error}
minTime={isSameDay && startDateTime ? startDateTime : null}
label="زمان پایان ماموریت"
placeholder="زمان پایان ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
)}
@@ -114,4 +114,5 @@ const MissionDates = ({ control }) => {
)
);
};
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 { Controller, useForm } from "react-hook-form";
import { array, object, string } from "yup";
import ItemSelectField from "./ItemSelectField";
import SubItemSelectField from "./SubItemSelectField";
const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"),
end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"),
category_id: string().required("نوع ماموریت را مشخص کنید!"),
item_id: string().required("موضوع را مشخص کنید!"),
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"),
road_observed_id: string().when("category_id", {
is: "3",
@@ -25,6 +28,8 @@ const validationSchema = object({
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = {
item_id: allData.item_id,
sub_item_id: allData.sub_item_id,
explanation: allData.explanation,
end_point: allData.end_point,
region: allData.region,
@@ -33,7 +38,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
road_observed_id: allData.road_observed_id,
};
const { control, handleSubmit } = useForm({
const { control, handleSubmit, watch, setValue, getValues } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
@@ -51,24 +56,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Controller
<ItemSelectField control={control} watch={watch} setValue={setValue} />
</Grid>
<Grid item xs={12} sm={6}>
<SubItemSelectField
control={control}
name={"explanation"}
render={({ field, fieldState: { error } }) => (
<PersianTextField
value={field.value}
variant="outlined"
name="explanation"
onChange={field.onChange}
label="موضوع"
placeholder={"موضوع را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
)}
watch={watch}
setValue={setValue}
getValues={getValues}
/>
</Grid>
<Grid item xs={12} sm={6}>

View File

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

View File

@@ -4,7 +4,7 @@ import ViolationsList from "./ViolationsList";
const ViolationsDialog = ({ open, setOpen, mutate }) => {
return (
<Dialog open={open} fullWidth maxWidth="xs">
<Dialog open={open} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpen(false)}

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;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
bottom: 12,
left: 12,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
backgroundColor: "rgba(255,255,255,0.95)",
backdropFilter: "blur(6px)",
border: "1px solid #ececec",
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
px: 1.25,
py: 0.75,
width: 230,
maxWidth: "70vw",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
<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)}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
))}
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
<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>
);

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 L from "leaflet";
import polyline from "@mapbox/polyline";
@@ -20,12 +20,22 @@ const ShowRoute = ({ area }) => {
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [area]);
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);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
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={destPosition} icon={destIcon} />
</>
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
};
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 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);
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
);
const isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
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) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
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() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
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() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
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));
}
},
});
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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);
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100);
} 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 = [
{ lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng },
];
try {
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
}
};
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);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition, waypoints);
}
}
};
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
setDestPosition(clickedPosition);
setIsDest(true);
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 = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
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) => {
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
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()}
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
icon={prevDestIcon}
/>
)}
{alternateCoords.map((coords, index) => (
<Polyline
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 { Controller, useForm } from "react-hook-form";
import { object, string } from "yup";
import FastReactCode from "./FastReactCode";
import ItemSelectField from "./ItemSelectField";
import SubItemSelectField from "./SubItemSelectField";
const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"),
item_id: string().required("موضوع را مشخص کنید!"),
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"),
category_id: string().required("نوع ماموریت را مشخص کنید!"),
@@ -24,6 +26,8 @@ const validationSchema = object({
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = {
item_id: allData.item_id,
sub_item_id: allData.sub_item_id,
explanation: allData.explanation,
end_point: allData.end_point,
region: allData.region,
@@ -31,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
road_observed_id: allData.road_observed_id,
};
const { control, handleSubmit } = useForm({
const { control, handleSubmit, watch, setValue, getValues } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
@@ -49,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Controller
<ItemSelectField control={control} watch={watch} setValue={setValue} />
</Grid>
<Grid item xs={12} sm={6}>
<SubItemSelectField
control={control}
name={"explanation"}
render={({ field, fieldState: { error } }) => (
<PersianTextField
value={field.value}
variant="outlined"
name="explanation"
onChange={field.onChange}
label="موضوع"
placeholder={"موضوع را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
)}
watch={watch}
setValue={setValue}
getValues={getValues}
/>
</Grid>
<Grid item xs={12} sm={6}>

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 { useReducer, useState } from "react";
import GetDateTime from "./GetDateTime";
import Area from "./Area";
import GetItemInfo from "./GetItemInfo";
import MachineAndDriver from "./MachineAndDriver";
import Rahdaran from "./Rahdaran";
import Verify from "./Verify";
import Area from "./Area";
function TabPanel(props) {
const { children, value, index } = props;

View File

@@ -26,6 +26,8 @@ const MissionCorrection = ({ row, mutate, baseMutate }) => {
end_point: result.end_point,
driver: result.driver.id,
encoded_route: result.area,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
},
hasSidebarUpdate: true,
})

View File

@@ -116,6 +116,54 @@ const ViolationsList = ({ setOpen, mutate }) => {
},
Cell: ({ row }) => <>{row.original.city_name}</>,
},
{
accessorKey: "enter_time",
header: "تاریخ و ساعت ورود",
id: "enter_time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 140,
Cell: ({ cell }) => {
const value = formatDateTime(cell.getValue());
if (!value) return "-";
return (
<>
<span className="font-medium">{value.date}</span>
<span> - </span>
<span className="text-xs text-muted-foreground">{value.time}</span>
</>
);
},
},
{
accessorKey: "exit_time",
header: "تاریخ و ساعت خروج",
id: "exit_time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 140,
Cell: ({ cell }) => {
const value = formatDateTime(cell.getValue());
if (!value) return "-";
return (
<div className="flex flex-col leading-tight">
<span className="font-medium">{value.date}</span>
<span> - </span>
<span className="text-xs text-muted-foreground">{value.time}</span>
</div>
);
},
},
];
}, []);
@@ -139,3 +187,22 @@ const ViolationsList = ({ setOpen, mutate }) => {
);
};
export default ViolationsList;
const formatDateTime = (value) => {
if (!value) return null;
const date = new Date(value.replace(" ", "T"));
return {
date: new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date),
time: new Intl.DateTimeFormat("fa-IR", {
hour: "2-digit",
minute: "2-digit",
}).format(date),
};
};

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;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
bottom: 12,
left: 12,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
backgroundColor: "rgba(255,255,255,0.95)",
backdropFilter: "blur(6px)",
border: "1px solid #ececec",
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
px: 1.25,
py: 0.75,
width: 230,
maxWidth: "70vw",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
<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)}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
))}
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
<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>
);

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 L from "leaflet";
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 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);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
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={destPosition} icon={destIcon} />
</>
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
};
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 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);
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
);
const isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
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) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
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() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
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() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
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));
}
},
});
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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);
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
}, 100);
} 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 = [
{ lat: source.lat, lon: source.lng },
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
{ lat: dest.lat, lon: dest.lng },
];
try {
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
}
};
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);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
if (wasEditing && destPosition) {
await fetchRoute(clickedPosition, destPosition, waypoints);
}
}
};
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
setDestPosition(clickedPosition);
setIsDest(true);
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 = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
resetRoute();
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
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) => {
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
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()}
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
icon={prevDestIcon}
/>
)}
{alternateCoords.map((coords, index) => (
<Polyline
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

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

View File

@@ -93,10 +93,12 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">خودرو</Typography>
<Divider sx={{ flex: 1 }} />
<Chip
size="small"
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
/>
{allData.machine && (
<Chip
size="small"
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
/>
)}
</Stack>
{/* <Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">راننده</Typography>

View File

@@ -9,6 +9,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
const requestServer = useRequest({ notificationSuccess: true });
const submitForm = async (result) => {
console.log("result: ", result);
setSubmitting(true);
await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", {
data: {
@@ -29,6 +31,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
end_point: result.end_point,
category_id: result.category_id,
explanation: result.explanation,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
...(result.category_id == 3
? {
road_observed_id: result.road_observed_id,
@@ -51,6 +55,8 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
<CreateForm
defaultValues={{
explanation: "",
item_id: "",
sub_item_id: "",
category_id: "",
rahdaran: rahdaran.filter((r) => !r.is_driver),
bound_type: "polyline",

View File

@@ -51,6 +51,8 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", {
data: {
explanation: result.explanation,
item_id: result.item_id,
sub_item_id: result.sub_item_id,
category_id: result.category_id,
...(result.category_id == 3
@@ -101,6 +103,8 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
<CreateForm
defaultValues={{
explanation: row.original.explanation,
item_id: row.original.item_id,
sub_item_id: row.original.sub_item_id,
category_id: row.original.category_id,
road_observed_id: row.original.category_id == 3 ? (row.original.road_observed?.[0]?.id ?? "") : "",

View File

@@ -4,16 +4,19 @@ import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
import RowActions from "./RowActions";
import { GET_DRIVERS_ACTIVITY_REPORT } from "@/core/utils/routes";
const DriverReportsList = ({ data, specialFilter }) => {
const DriverReportsList = ({ specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "driver_id",
header: "کد یکتا",
id: "driver_id",
enableColumnFilter: false,
enableSorting: false,
enableColumnFilter: true,
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "numeric",
grow: false,
size: 50,
@@ -22,8 +25,10 @@ const DriverReportsList = ({ data, specialFilter }) => {
accessorKey: "driver_name",
header: "نام راننده",
id: "driver_name",
enableColumnFilter: false,
enableSorting: false,
enableColumnFilter: true,
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "text",
grow: false,
size: 50,
@@ -43,26 +48,28 @@ const DriverReportsList = ({ data, specialFilter }) => {
header: "تعداد ماموریت",
id: "missions",
enableColumnFilter: false,
enableSorting: false,
enableSorting: true,
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}
need_filter={true}
columns={columns}
data={data.data}
table_url={GET_DRIVERS_ACTIVITY_REPORT}
page_name={"driverReport"}
table_name={"driverReportList"}
enablePagination={false}
specialFilter={specialFilter}
enableRowActions
RowActions={RowActions}
RowActions={RowActionsWithRowData}
/>
</Box>
);

View File

@@ -1,8 +1,5 @@
"use client";
import {
EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_DRIVER_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes";
import { EXPORT_GET_DRIVER_ACTIVITY_REPORT } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { useTheme } from "@emotion/react";
import DescriptionIcon from "@mui/icons-material/Description";
@@ -34,11 +31,7 @@ const PrintExcel = ({ table, filterData }) => {
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;
const requestUrl = EXPORT_GET_DRIVER_ACTIVITY_REPORT;
requestServer(`${requestUrl}?${filterParams}`, "get", {
requestOptions: { responseType: "blob" },
})
@@ -66,7 +59,7 @@ const PrintExcel = ({ table, filterData }) => {
disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
onClick={clickHandler}
// onClick={clickHandler}
>
خروجی اکسل
</Button>

View File

@@ -0,0 +1,79 @@
"use client";
import { EXPORT_GET_DRIVER_DETAILS_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, specialFilterData }) => {
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 activeSpecialFilters = Object.entries(specialFilterData).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);
});
activeSpecialFilters.length > 0 &&
activeSpecialFilters.map((filter, index) => {
params.set(`${filter.id}`, filter.value);
});
return params;
}, [activeFilters, activeSpecialFilters]);
const clickHandler = () => {
setLoading(true);
const requestUrl = EXPORT_GET_DRIVER_DETAILS_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,11 @@
import { Box } from "@mui/material";
import PrintExcel from "./ExcelPrint";
const Toolbar = ({ table, filterData, specialFilterData, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1 }}>
<PrintExcel table={table} filterData={filterData} specialFilterData={specialFilterData} />
</Box>
);
};
export default Toolbar;

View File

@@ -1,11 +1,52 @@
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_DRIVER_DETAILS } from "@/core/utils/routes";
import useCities from "@/lib/hooks/useCities";
import useProvinces from "@/lib/hooks/useProvince";
import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
const DriverList = ({ row }) => {
const columns = useMemo(
() => [
const DriverList = ({ row, specialFilter }) => {
const columns = useMemo(() => {
const dynamicColumns = {
header: "استان",
id: "province_id",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 130,
ColumnSelectComponent: (props) => {
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const getColumnSelectOptions = useMemo(() => {
if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل کشور" },
...provinces.map((province) => ({
value: province.id,
label: province.name_fa,
})),
];
}, [provinces, errorProvinces, loadingProvinces]);
return (
<CustomSelectByDependency
{...props}
value={loadingProvinces ? "loading" : props.filterParameters.value}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ renderedCellValue, row }) => {
return <>{row.original.province_name}</>;
},
};
return [
{
accessorKey: "id",
header: "کد یکتا",
@@ -17,27 +58,52 @@ const DriverList = ({ row }) => {
grow: false,
size: 80,
},
...[dynamicColumns],
{
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",
id: "city_id",
enableColumnFilter: true,
datatype: "text",
datatype: "numeric",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
dependencyId: "province_id",
grow: false,
size: 100,
size: 120,
ColumnSelectComponent: (props) => {
const { cityList, loadingCityList, errorCityList } = useCities(props.dependencyFieldValue.value);
const getColumnSelectOptions = useMemo(() => {
if (props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingCityList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorCityList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...cityList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [cityList, loadingCityList, errorCityList]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingCityList
? "loading"
: props.filterParameters.value
}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ row }) => <>{row.original.city_name}</>,
},
{
accessorKey: "station_name",
@@ -83,18 +149,22 @@ const DriverList = ({ row }) => {
grow: false,
size: 100,
},
],
[]
);
];
}, []);
return (
<>
<Box sx={{ mt: 3 }}>
<DataTableWithAuth
TableToolbar={Toolbar}
need_filter={true}
columns={columns}
specialFilter={specialFilter}
specialQueries={{
driver_id: row.driver_id,
}}
sorting={[{ id: "id", desc: true }]}
table_url={`${GET_DRIVER_DETAILS}/${row.driver_id}`}
table_url={GET_DRIVER_DETAILS}
page_name={"driverListReport"}
table_name={"driverList"}
positionActionsColumn={"first"}

View File

@@ -2,14 +2,14 @@ import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import { Stack } from "@mui/system";
import DriverList from "./DriverDetailsDialog/DriverList";
const DriverDetailsTable = ({ row, setOpenDriverDetails }) => {
const DriverDetailsTable = ({ row, setOpenDriverDetails, specialFilter }) => {
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack spacing={3}>
<Stack spacing={2}>
<DriverList row={row} />
<DriverList specialFilter={specialFilter} row={row} />
</Stack>
</Stack>
</Box>

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import DriverDetailsTable from "./Form";
const DriverDetails = ({ row }) => {
const DriverDetails = ({ row, specialFilter }) => {
const [openDriverDetails, setOpenDriverDetails] = useState(false);
return (
@@ -35,7 +35,11 @@ const DriverDetails = ({ row }) => {
</IconButton>
<DialogTitle>ریز گزارشات راننده</DialogTitle>
{openDriverDetails && (
<DriverDetailsTable row={row.original} setOpenDriverDetails={setOpenDriverDetails} />
<DriverDetailsTable
specialFilter={specialFilter}
row={row.original}
setOpenDriverDetails={setOpenDriverDetails}
/>
)}
</Dialog>
</>

View File

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

View File

@@ -1,11 +1,9 @@
"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 { Stack } from "@mui/material";
import moment from "jalali-moment";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -14,10 +12,8 @@ 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 = {
@@ -27,43 +23,37 @@ const DriverReportsPage = () => {
};
const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${defaultValues.date_to}`, datatype: "date", id: "date_to", fn: "between" },
{ 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}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${data.date_to}`, datatype: "date", id: "date_to", fn: "between" },
]);
if (data.province_id === "-1") {
try {
const response = await requestServer(`${GET_DRIVER_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
const onSearchSubmit = (data) => {
const filters = [
{
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",
},
];
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) {}
if (data.province_id !== "-1") {
filters.unshift({
value: data.province_id,
datatype: "numeric",
id: "province_id",
fn: "equals",
});
}
setSpecialFilter(filters);
};
useEffect(() => {
@@ -100,7 +90,7 @@ const DriverReportsPage = () => {
handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit}
/>
{!data ? <LinearProgress /> : <DriverReportsList specialFilter={specialFilter} data={data} />}
<DriverReportsList specialFilter={specialFilter} />
</Stack>
);
};

View File

@@ -1,8 +1,5 @@
"use client";
import {
EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes";
import { EXPORT_GET_MACHINES_ACTIVITY_REPORT } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { useTheme } from "@emotion/react";
import DescriptionIcon from "@mui/icons-material/Description";
@@ -34,16 +31,12 @@ const PrintExcel = ({ table, filterData }) => {
const clickHandler = () => {
setLoading(true);
const CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl =
CountryOrProvince.value === "-1"
? EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT;
const requestUrl = EXPORT_GET_MACHINES_ACTIVITY_REPORT;
requestServer(`${requestUrl}?${filterParams}`, "get", {
requestOptions: { responseType: "blob" },
})
.then((response) => {
const filename = `خروجی کارتابل گزارشات گشت راهداری تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
const filename = `خروجی کارتابل گزارشات ماشین ها تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
FileSaver.saveAs(response.data, filename);
})
.catch(() => {})
@@ -66,7 +59,7 @@ const PrintExcel = ({ table, filterData }) => {
disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
onClick={clickHandler}
// onClick={clickHandler}
>
خروجی اکسل
</Button>

View File

@@ -4,17 +4,24 @@ import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
import RowActions from "./RowActions";
import {
GET_MACHINES_ACTIVITY_REPORT,
GET_MACHINES_COUNTRY_ACTIVITY_REPORT,
GET_MACHINES_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes";
const MachinesReportsList = ({ data, specialFilter }) => {
const MachinesReportsList = ({ specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "machine_id",
header: "کد یکتا",
id: "machine_id",
enableColumnFilter: false,
enableSorting: false,
datatype: "numeric",
enableColumnFilter: true,
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "text",
grow: false,
size: 50,
},
@@ -23,7 +30,9 @@ const MachinesReportsList = ({ data, specialFilter }) => {
header: "مدل ماشین",
id: "car_name",
enableColumnFilter: false,
enableSorting: false,
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "text",
grow: false,
size: 50,
@@ -32,7 +41,8 @@ const MachinesReportsList = ({ data, specialFilter }) => {
accessorKey: "machine_code",
header: "کد ماشین",
id: "machine_code",
enableColumnFilter: false,
enableColumnFilter: true,
columnFilterModeOptions: ["equals", "contains"],
enableSorting: false,
datatype: "text",
grow: false,
@@ -43,7 +53,8 @@ const MachinesReportsList = ({ data, specialFilter }) => {
header: "کارکرد",
id: "func",
enableColumnFilter: false,
enableSorting: false,
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "number",
grow: false,
size: 50,
@@ -53,26 +64,28 @@ const MachinesReportsList = ({ data, specialFilter }) => {
header: "تعداد ماموریت",
id: "missions",
enableColumnFilter: false,
enableSorting: false,
enableSorting: true,
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}
table_url={GET_MACHINES_ACTIVITY_REPORT}
page_name={"roadMissionReport"}
table_name={"roadMissionReportList"}
enablePagination={false}
enablePagination={true}
specialFilter={specialFilter}
need_filter={true}
enableRowActions
RowActions={RowActions}
RowActions={RowActionsWithRowData}
/>
</Box>
);

View File

@@ -0,0 +1,70 @@
"use client";
import { EXPORT_GET_MACHINES_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 requestUrl = EXPORT_GET_MACHINES_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,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

@@ -1,11 +1,52 @@
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_MACHINE_MISSIONS_DETAILS } from "@/core/utils/routes";
import useCities from "@/lib/hooks/useCities";
import useProvinces from "@/lib/hooks/useProvince";
import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
const MachineMissionsList = ({ row }) => {
const columns = useMemo(
() => [
const MachineMissionsList = ({ row, specialFilter }) => {
const columns = useMemo(() => {
const dynamicColumns = {
header: "استان",
id: "province_id",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 130,
ColumnSelectComponent: (props) => {
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const getColumnSelectOptions = useMemo(() => {
if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل کشور" },
...provinces.map((province) => ({
value: province.id,
label: province.name_fa,
})),
];
}, [provinces, errorProvinces, loadingProvinces]);
return (
<CustomSelectByDependency
{...props}
value={loadingProvinces ? "loading" : props.filterParameters.value}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ renderedCellValue, row }) => {
return <>{row.original.province_name}</>;
},
};
return [
{
accessorKey: "id",
header: "کد یکتا",
@@ -17,27 +58,52 @@ const MachineMissionsList = ({ row }) => {
grow: false,
size: 80,
},
...[dynamicColumns],
{
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",
id: "city_id",
enableColumnFilter: true,
datatype: "text",
datatype: "numeric",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
dependencyId: "province_id",
grow: false,
size: 100,
size: 120,
ColumnSelectComponent: (props) => {
const { cityList, loadingCityList, errorCityList } = useCities(props.dependencyFieldValue.value);
const getColumnSelectOptions = useMemo(() => {
if (props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingCityList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorCityList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...cityList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [cityList, loadingCityList, errorCityList]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingCityList
? "loading"
: props.filterParameters.value
}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ row }) => <>{row.original.city_name}</>,
},
{
accessorKey: "station_name",
@@ -83,14 +149,15 @@ const MachineMissionsList = ({ row }) => {
grow: false,
size: 100,
},
],
[]
);
];
}, []);
return (
<>
<Box sx={{ mt: 3 }}>
<DataTableWithAuth
specialFilter={specialFilter}
TableToolbar={Toolbar}
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}

View File

@@ -2,14 +2,14 @@ import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import { Stack } from "@mui/system";
import MachineMissionsList from "./MachineMissionsDetailsDialog/MachineMissionsList";
const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails }) => {
const MachineMissionsDetailsTable = ({ row, setOpenMachineMissionsDetails, specialFilter }) => {
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack spacing={3}>
<Stack spacing={2}>
<MachineMissionsList row={row} />
<MachineMissionsList specialFilter={specialFilter} row={row} />
</Stack>
</Stack>
</Box>

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import MachineMissionsDetailsTable from "./Form";
const MissionDetails = ({ row }) => {
const MissionDetails = ({ row, specialFilter }) => {
const [openMachineMissionsDetails, setOpenMachineMissionsDetails] = useState(false);
return (
@@ -36,6 +36,7 @@ const MissionDetails = ({ row }) => {
<DialogTitle>ریز گزارشات خودرو</DialogTitle>
{openMachineMissionsDetails && (
<MachineMissionsDetailsTable
specialFilter={specialFilter}
row={row.original}
setOpenMachineMissionsDetails={setOpenMachineMissionsDetails}
/>

View File

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

View File

@@ -1,11 +1,9 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { GET_MACHINES_COUNTRY_ACTIVITY_REPORT, GET_MACHINES_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 { Stack } from "@mui/material";
import moment from "jalali-moment";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -14,10 +12,8 @@ import MachinesReportsList from "./MachinesReportsList";
import SearchReportList from "./Search";
const MachinesReportsPage = () => {
const [data, setData] = useState(null);
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const requestServer = useRequest();
const hasProvincesPermission = userPermissions?.includes("mission-report-country");
const defaultValues = {
@@ -27,43 +23,37 @@ const MachinesReportsPage = () => {
};
const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${defaultValues.date_to}`, datatype: "date", id: "date_to", fn: "between" },
{ 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}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${data.date_to}`, datatype: "date", id: "date_to", fn: "between" },
]);
if (data.province_id === "-1") {
try {
const response = await requestServer(`${GET_MACHINES_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
const onSearchSubmit = (data) => {
const filters = [
{
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",
},
];
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
} else {
try {
params.set("province_id", `${data.province_id}`);
const response = await requestServer(`${GET_MACHINES_PROVINCE_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
if (data.province_id !== "-1") {
filters.unshift({
value: data.province_id,
datatype: "numeric",
id: "province_id",
fn: "equals",
});
}
setSpecialFilter(filters);
};
useEffect(() => {
@@ -100,7 +90,7 @@ const MachinesReportsPage = () => {
handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit}
/>
{!data ? <LinearProgress /> : <MachinesReportsList specialFilter={specialFilter} data={data} />}
<MachinesReportsList specialFilter={specialFilter} />
</Stack>
);
};

View File

@@ -1,10 +1,5 @@
"use client";
import {
EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT,
} from "@/core/utils/routes";
import { EXPORT_GET_MACHINES_TYPE_ACTIVITY_REPORT } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { useTheme } from "@emotion/react";
import DescriptionIcon from "@mui/icons-material/Description";
@@ -36,16 +31,12 @@ const PrintExcel = ({ table, filterData }) => {
const clickHandler = () => {
setLoading(true);
const CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl =
CountryOrProvince.value === "-1"
? EXPORT_GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT;
const requestUrl = EXPORT_GET_MACHINES_TYPE_ACTIVITY_REPORT;
requestServer(`${requestUrl}?${filterParams}`, "get", {
requestOptions: { responseType: "blob" },
})
.then((response) => {
const filename = `خروجی کارتابل گزارشات گشت راهداری تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
const filename = `خروجی کارتابل گزارشات نوع ماشین تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
FileSaver.saveAs(response.data, filename);
})
.catch(() => {})
@@ -68,7 +59,7 @@ const PrintExcel = ({ table, filterData }) => {
disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
onClick={clickHandler}
// onClick={clickHandler}
>
خروجی اکسل
</Button>

View File

@@ -3,17 +3,20 @@ import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
// import RowActions from "./RowActions";
import { GET_MACHINES_TYPE_ACTIVITY_REPORT } from "@/core/utils/routes";
import RowActions from "./RowActions";
const MachinesReportsList = ({ data, specialFilter }) => {
const MachinesReportsList = ({ specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "car_type",
header: "نوع ماشین",
id: "car_type",
enableColumnFilter: false,
enableSorting: false,
enableColumnFilter: true,
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
enableSorting: true,
datatype: "text",
grow: false,
size: 50,
@@ -23,7 +26,7 @@ const MachinesReportsList = ({ data, specialFilter }) => {
header: "کارکرد",
id: "func",
enableColumnFilter: false,
enableSorting: false,
enableSorting: true,
datatype: "number",
grow: false,
size: 50,
@@ -33,7 +36,7 @@ const MachinesReportsList = ({ data, specialFilter }) => {
header: "تعداد ماموریت",
id: "missions",
enableColumnFilter: false,
enableSorting: false,
enableSorting: true,
datatype: "number",
grow: false,
size: 50,
@@ -46,13 +49,14 @@ const MachinesReportsList = ({ data, specialFilter }) => {
<DataTableWithAuth
TableToolbar={Toolbar}
columns={columns}
data={data.data}
table_url={GET_MACHINES_TYPE_ACTIVITY_REPORT}
page_name={"roadMissionReportMachineTypeReport"}
table_name={"roadMissionReportMachineTypeReportList"}
enablePagination={false}
specialFilter={specialFilter}
// enableRowActions
// RowActions={RowActions}
need_filter={true}
enableRowActions
RowActions={RowActions}
/>
</Box>
);

View File

@@ -0,0 +1,70 @@
"use client";
import { EXPORT_GET_MACHINES_TYPE_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 requestUrl = EXPORT_GET_MACHINES_TYPE_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,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

@@ -1,7 +1,8 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_MACHINE_MISSIONS_DETAILS, GET_MACHINE_TYPE_MISSIONS_DETAILS } from "@/core/utils/routes";
import { GET_MACHINE_TYPE_MISSIONS_DETAILS } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
const MachineMissionsList = ({ row }) => {
const columns = useMemo(
@@ -91,10 +92,14 @@ const MachineMissionsList = ({ row }) => {
<>
<Box sx={{ mt: 3 }}>
<DataTableWithAuth
TableToolbar={Toolbar}
need_filter={true}
columns={columns}
specialQueries={{
car_type: row.car_type,
}}
sorting={[{ id: "id", desc: true }]}
table_url={`${GET_MACHINE_TYPE_MISSIONS_DETAILS}?car_type=${row.car_type}`}
table_url={GET_MACHINE_TYPE_MISSIONS_DETAILS}
page_name={"roadMissionReportMachineType"}
table_name={"roadMissionReportMachineTypeList"}
positionActionsColumn={"first"}

View File

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

View File

@@ -1,14 +1,9 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import {
GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT,
GET_MACHINES_TYPE_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 { Stack } from "@mui/material";
import moment from "jalali-moment";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -17,10 +12,8 @@ import MachinesReportsList from "./MachinesReportsList";
import SearchReportList from "./Search";
const MachinesTypeReportsPage = () => {
const [data, setData] = useState(null);
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const requestServer = useRequest();
const hasProvincesPermission = userPermissions?.includes("mission-report-country");
const defaultValues = {
@@ -30,43 +23,37 @@ const MachinesTypeReportsPage = () => {
};
const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${defaultValues.date_to}`, datatype: "date", id: "date_to", fn: "between" },
{ 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}`, datatype: "date", id: "from_date", fn: "between" },
{ value: `${data.date_to}`, datatype: "date", id: "date_to", fn: "between" },
]);
if (data.province_id === "-1") {
try {
const response = await requestServer(`${GET_MACHINES_TYPE_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
const onSearchSubmit = (data) => {
const filters = [
{
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",
},
];
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
} else {
try {
params.set("province_id", `${data.province_id}`);
const response = await requestServer(`${GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
if (data.province_id !== "-1") {
filters.unshift({
value: data.province_id,
datatype: "numeric",
id: "province_id",
fn: "equals",
});
}
setSpecialFilter(filters);
};
useEffect(() => {
@@ -103,7 +90,7 @@ const MachinesTypeReportsPage = () => {
handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit}
/>
{!data ? <LinearProgress /> : <MachinesReportsList specialFilter={specialFilter} data={data} />}
<MachinesReportsList specialFilter={specialFilter} />
</Stack>
);
};

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 L from "leaflet";
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 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);
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
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={destPosition} icon={destIcon} />
</>

View File

@@ -0,0 +1,42 @@
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
import React, { useState } from "react";
import useRequest from "@/lib/hooks/useRequest";
import { DELETE_VIOLATION } from "@/core/utils/routes";
const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const handleClick = () => {
setSubmitting(true);
requestServer(`${DELETE_VIOLATION}/${rowId}`, "delete", {
hasSidebarUpdate: true,
})
.then(() => {
mutate();
setOpenDeleteDialog(false);
setSubmitting(false);
})
.catch(() => {
setSubmitting(false);
});
};
return (
<>
<DialogContent>
<Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از حذف تخلف اطمینان دارید؟</Typography>
</Stack>
</DialogContent>
<DialogActions sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}>
<Button onClick={() => setOpenDeleteDialog(false)} variant="outlined" color="secondary">
خیر !
</Button>
<Button onClick={handleClick} variant="contained" color="error" disabled={submitting}>
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
</Button>
</DialogActions>
</>
);
};
export default DeleteContent;

View File

@@ -0,0 +1,33 @@
import { useState } from "react";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import DeleteContent from "./DeleteContent";
const DeleteViolation = ({ mutate, rowId }) => {
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
return (
<>
<Tooltip title="حذف">
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
<DeleteIcon />
</IconButton>
</Tooltip>
<Dialog
open={openDeleteDialog}
onClose={() => setOpenDeleteDialog(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={"md"}
>
<DialogTitle>حذف تخلف</DialogTitle>
<DeleteContent rowId={rowId} mutate={mutate} setOpenDeleteDialog={setOpenDeleteDialog} />
</Dialog>
</>
);
};
export default DeleteViolation;

View File

@@ -1,5 +1,6 @@
// import CreateWithoutProcess from "../Actions/CreateWithoutProcess";
import EditViolation from "../Actions/EditViolation";
import DeleteViolation from "../Actions/DeleteViolation";
const RowActions = ({ row, mutate }) => {
// const canEdit = row.original.status === 0 && row.original.type === 1;
@@ -7,6 +8,7 @@ const RowActions = ({ row, mutate }) => {
<>
{!row.original.mileage && <EditViolation row={row} mutate={mutate} rowId={row.getValue("id")} />}
{/*{canEdit && <CreateWithoutProcess row={row} mutate={mutate} />}*/}
<DeleteViolation rowId={row.getValue("id")} mutate={mutate} />
</>
);
};

View File

@@ -61,23 +61,6 @@ const ViolationsList = () => {
columnFilterModeOptions: ["equals", "contains"],
grow: false,
},
{
accessorKey: "status",
header: "وضعیت",
id: "status",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
sortDescFirst: true,
grow: false,
columnSelectOption: () => {
return violationsCategoryStatus.map((category) => ({
value: category.id,
label: category.name_fa,
}));
},
Cell: ({ row }) => (row.original.type === 1 ? "خروج بدون مجوز" : "عدم تحرک"),
},
{
accessorKey: "machine_code",
header: "کد خودرو",
@@ -142,16 +125,6 @@ const ViolationsList = () => {
grow: false,
Cell: ({ row }) => row.original.machine?.car_name || "-",
},
{
accessorKey: "mission_id",
header: "کد ماموریت",
id: "mission_id",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
Cell: ({ row }) => row.original.mission_id || "-",
},
{
accessorKey: "request_date",
header: "تاریخ تخلف",

View File

@@ -38,6 +38,7 @@ const DataTable_Main = (props) => {
RowActions,
specific_data,
specialFilter,
specialQueries,
} = props;
const flatColumns = flattenArrayOfObjects(columns, "columns");
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
@@ -70,8 +71,15 @@ const DataTable_Main = (props) => {
}))
)
);
if (specialQueries) {
Object.entries(specialQueries).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
params.set(key, String(value));
}
});
}
return `${table_url}?${params}`;
}, [table_url, filterData, pagination, sortData, specialFilter]);
}, [table_url, filterData, pagination, sortData, specialFilter, specialQueries]);
const fetcher = async (url) => {
try {
@@ -152,7 +160,12 @@ const DataTable_Main = (props) => {
renderTopToolbarCustomActions: ({ table }) => (
<>
{TableToolbar && (
<TableToolbar table={table} mutate={mutate} filterData={specialFilter || filterData} />
<TableToolbar
table={table}
mutate={mutate}
filterData={specialFilter || filterData}
specialFilterData={filterData}
/>
)}
</>
),

View File

@@ -249,24 +249,28 @@ export const FINISH_MISSION = api + "/api/v3/missions/control_unit/finish";
export const GET_ROAD_MISSIONS_VIOLATIONS_LIST = api + "/api/v3/missions/violation_management";
export const GET_MISSION_PROVINCE_ACTIVITY_REPORT = api + "/api/v3/missions/report/country_activity";
export const GET_MISSION_CITY_ACTIVITY_REPORT = api + "/api/v3/missions/report/country_activity";
export const GET_MACHINES_COUNTRY_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/country_machines_activity";
export const GET_MACHINES_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/machines_activity";
export const GET_MACHINES_PROVINCE_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/province_machines_activity";
export const EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machines_activity_excel";
export const EXPORT_GET_MACHINES_TYPE_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/machines_activity_excel";
export const EXPORT_GET_MACHINES_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machines_activity_excel";
export const EXPORT_GET_MACHINES_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/machines_activity_excel";
export const GET_DRIVER_COUNTRY_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/country_machine_drivers_activity";
export const GET_DRIVERS_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/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_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/machine_drivers_activity_excel";
export const EXPORT_GET_DRIVER_DETAILS_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/machine_driver_details_report_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 =
api + "/api/v3/missions/report_machine/country_machine_types_activity";
export const GET_MACHINES_TYPE_ACTIVITY_REPORT = api + "/api/v3/missions/report_machine/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 =
@@ -275,6 +279,7 @@ export const EXPORT_GET_MACHINES_TYPE_PROVINCE_ACTIVITY_REPORT =
api + "/api/v3/missions/report_machine/province_machine_types_activity_excel";
export const CREATE_VIOLATION = api + "/api/v3/missions/violation_management";
export const EDIT_VIOLATION = api + "/api/v3/missions/violation_management";
export const DELETE_VIOLATION = api + "/api/v3/missions/violation_management";
export const EXPORT_COUNTRY_ROAD_MISSIONS_REPORTS = api + "/api/v3/missions/report/country_excel_activity";
export const EXPORT_PROVINCE_ROAD_MISSIONS_REPORTS = api + "/api/v3/missions/report/province_excel_activity";