Merge branch 'release/v1.4.0'

This commit is contained in:
Amirhossein Mahmoodi
2025-06-25 11:14:03 +03:30
735 changed files with 48931 additions and 46049 deletions

View File

@@ -1,4 +1,4 @@
HOST="rms.witel.ir" HOST="rms.witel.ir"
NEXT_PUBLIC_VERSION="1.3.9" NEXT_PUBLIC_VERSION="1.4.0"
NEXT_PUBLIC_API_URL="https://rms.witel.ir" NEXT_PUBLIC_API_URL="https://rms.witel.ir"
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map" NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"

View File

@@ -0,0 +1,17 @@
import ControlPage from "@/components/dashboard/roadMissions/control";
export const metadata = {
title: "کارتابل کنترل",
};
const Page = () => {
return (
<>
{/* <WithPermission permission_name={["create-road-item"]}> */}
<ControlPage />
{/* <ActivityCodeLog activity_code={1152} /> */}
{/* </WithPermission> */}
</>
);
};
export default Page;

View File

@@ -0,0 +1,18 @@
import WithPermission from "@/core/middlewares/withPermission";
import ActivityCodeLog from "@/core/components/ActivityCodeLog";
import OperatorPage from "@/components/dashboard/roadMissions/operator";
export const metadata = {
title: "کارتابل ماموریت",
};
const Page = () => {
return (
<>
{/* <WithPermission permission_name={["create-road-item"]}> */}
<OperatorPage />
{/* <ActivityCodeLog activity_code={1152} /> */}
{/* </WithPermission> */}
</>
);
};
export default Page;

View File

@@ -0,0 +1,17 @@
import TransportationPage from "@/components/dashboard/roadMissions/transportation";
export const metadata = {
title: "کارتابل نقلیه",
};
const Page = () => {
return (
<>
{/* <WithPermission permission_name={["create-road-item"]}> */}
<TransportationPage />
{/* <ActivityCodeLog activity_code={1152} /> */}
{/* </WithPermission> */}
</>
);
};
export default Page;

View File

@@ -1,5 +1,6 @@
@import "fontiran"; @import "fontiran";
@import "leaflet/dist/leaflet.css"; @import "leaflet/dist/leaflet.css";
@import "leaflet-draw/dist/leaflet.draw.css";
@import "react-toastify/dist/ReactToastify.css"; @import "react-toastify/dist/ReactToastify.css";
@import "./map-styles.scss"; @import "./map-styles.scss";

View File

@@ -7,3 +7,23 @@
font-size: 14px; font-size: 14px;
line-height: 40px; line-height: 40px;
} }
.leaflet-draw {
display: none;
}
.leaflet-draw-tooltip {
margin-left: 0;
margin-right: 20px;
}
.leaflet-draw-tooltip::before {
border-left: 6px solid black;
border-left-color: rgba(0, 0, 0, 0.5);
border-right: 0;
right: -7px;
left: auto;
}
.leaflet-editing-icon {
border-radius: 50%;
}

View File

@@ -0,0 +1,55 @@
import MapLayer from "@/core/components/MapLayer";
import { Close, RemoveRedEye } from "@mui/icons-material";
import { Box, Dialog, DialogContent, DialogTitle, IconButton, Tooltip, Typography } from "@mui/material";
import { useMemo, useState } from "react";
import ShowBound from "../showBound";
import L from "leaflet";
const ShowArea = ({ area }) => {
const bound = useMemo(() => L.polygon([...area]), [area]);
const [openAreaDialog, setOpenAreaDialog] = useState(false);
return (
<>
<Tooltip title="محدوده">
<IconButton color="primary" onClick={() => setOpenAreaDialog(true)}>
<RemoveRedEye />
</IconButton>
</Tooltip>
<Dialog
open={openAreaDialog}
onClose={() => setOpenAreaDialog(false)}
PaperProps={{
sx: {
transition: "all .3s",
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
borderRadius: 2,
padding: 1,
},
}}
maxWidth="sm"
fullWidth
dir="rtl"
>
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
محدوده
</Typography>
<IconButton onClick={() => setOpenAreaDialog(false)} size="small">
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ flex: 1 }}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
</Box>
</Box>
</DialogContent>
</Dialog>
</>
);
};
export default ShowArea;

View File

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

View File

@@ -0,0 +1,125 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_ROAD_MISSIONS_CONTROL_LIST } from "@/core/utils/routes";
import { Box, Stack } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
import ShowArea from "./Actions/showArea";
import RowActions from "./RowActions";
const ControlList = () => {
const typeOptions = [
{ value: 1, label: "روزانه" },
{ value: 2, label: "ساعتی" },
];
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "type",
header: "نوع",
id: "type",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
sortDescFirst: true,
grow: false,
size: 100,
columnSelectOption: () => {
return typeOptions.map((type) => ({
value: type.value,
label: type.label,
}));
},
Cell: ({ row }) => row.original.type_fa,
},
{
accessorFn: (row) => moment(row.start_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ شروع",
id: "start_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorFn: (row) => moment(row.end_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ پایان",
id: "end_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorKey: "end_point",
header: "مقصد",
id: "end_point",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
},
{
header: "محدوده",
id: "area",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
py: 0,
"&:first-of-type": {
borderLeft: "unset",
},
},
},
Cell: ({ row }) =>
row.original.area ? (
<Stack alignItems={"center"} justifyContent={"center"}>
<ShowArea area={row.original.area} />
</Stack>
) : (
"-"
),
},
],
[]
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_ROAD_MISSIONS_CONTROL_LIST}
page_name={"roadMissionsControl"}
table_name={"roadMissionsControlList"}
enableRowActions
positionActionsColumn={"first"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default ControlList;

View File

@@ -0,0 +1,42 @@
import { FINISH_MISSION } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
import { useState } from "react";
const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const handleClick = () => {
setSubmitting(true);
requestServer(`${FINISH_MISSION}/${rowId}`, "post", {
hasSidebarUpdate: true,
})
.then(() => {
mutate();
setOpenFinishMissionDialog(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={() => setOpenFinishMissionDialog(false)} variant="outlined" color="secondary">
خیر !
</Button>
<Button onClick={handleClick} variant="contained" color="primary" disabled={submitting}>
{submitting ? "درحال ثبت..." : "بله اطمینان دارم"}
</Button>
</DialogActions>
</>
);
};
export default FinishMissionContent;

View File

@@ -0,0 +1,36 @@
import { Login } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import FinishMissionContent from "./FinishMissionContent";
const FinishMission = ({ rowId, mutate }) => {
const [openFinishMissionDialog, setOpenFinishMissionDialog] = useState(false);
return (
<>
<Tooltip title="ورود">
<IconButton color="primary" onClick={() => setOpenFinishMissionDialog(true)}>
<Login />
</IconButton>
</Tooltip>
<Dialog
open={openFinishMissionDialog}
onClose={() => setOpenFinishMissionDialog(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>
<FinishMissionContent
rowId={rowId}
mutate={mutate}
setOpenFinishMissionDialog={setOpenFinishMissionDialog}
/>
</Dialog>
</>
);
};
export default FinishMission;

View File

@@ -0,0 +1,42 @@
import { START_MISSION } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
import { useState } from "react";
const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const handleClick = () => {
setSubmitting(true);
requestServer(`${START_MISSION}/${rowId}`, "post", {
hasSidebarUpdate: true,
})
.then(() => {
mutate();
setOpenStartMissionDialog(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={() => setOpenStartMissionDialog(false)} variant="outlined" color="secondary">
خیر !
</Button>
<Button onClick={handleClick} variant="contained" color="primary" disabled={submitting}>
{submitting ? "درحال ثبت..." : "بله اطمینان دارم"}
</Button>
</DialogActions>
</>
);
};
export default StartMissionContent;

View File

@@ -0,0 +1,36 @@
import { Logout } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import StartMissionContent from "./StartMissionContent";
const StartMission = ({ rowId, mutate }) => {
const [openStartMissionDialog, setOpenStartMissionDialog] = useState(false);
return (
<>
<Tooltip title="خروج">
<IconButton color="primary" onClick={() => setOpenStartMissionDialog(true)}>
<Logout />
</IconButton>
</Tooltip>
<Dialog
open={openStartMissionDialog}
onClose={() => setOpenStartMissionDialog(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>
<StartMissionContent
rowId={rowId}
mutate={mutate}
setOpenStartMissionDialog={setOpenStartMissionDialog}
/>
</Dialog>
</>
);
};
export default StartMission;

View File

@@ -0,0 +1,13 @@
import { Box } from "@mui/material";
import FinishMission from "./FinishMission";
import StartMission from "./StartMission";
const RowActions = ({ row, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1 }}>
{row.original.state_id == 2 && <StartMission rowId={row.getValue("id")} mutate={mutate} />}
{row.original.state_id == 3 && <FinishMission rowId={row.getValue("id")} mutate={mutate} />}
</Box>
);
};
export default RowActions;

View File

@@ -0,0 +1,13 @@
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import ControlList from "./ControlList";
const ControlPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"کارتابل کنترل"} />
<ControlList />
</Stack>
);
};
export default ControlPage;

View File

@@ -0,0 +1,116 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import L from "leaflet";
import { FeatureGroup, useMap } from "react-leaflet";
import "leaflet-draw";
const DrawBound = ({ control, controlDispach, bound, setBound }) => {
const map = useMap();
const featureRef = useRef(null);
const [area, setArea] = useState();
const drawControlRef = useRef(null);
const safeFlyToBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.flyToBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("flyToBounds failed:", e);
}
};
const bindEditEvent = (layer) => {
if (!layer) return;
layer.on("edit", function (e) {
const editedLayer = e.target;
setArea(editedLayer);
setBound(editedLayer);
safeFlyToBounds(editedLayer);
});
};
const handlerCreatedBound = useCallback((event) => {
const { layer } = event;
layer.editing.enable();
featureRef.current.addLayer(layer); // 🟢 اول اضافه به نقشه
setArea(layer);
setBound(layer);
safeFlyToBounds(layer);
bindEditEvent(layer);
controlDispach({ type: "SET_STATUS", status: 2 });
}, []);
useEffect(() => {
if (
control.status === 1 &&
drawControlRef.current &&
drawControlRef.current._toolbars?.draw?._modes?.polygon?.handler
) {
drawControlRef.current._toolbars.draw._modes.polygon.handler.enable();
}
}, [control.status]);
useEffect(() => {
if (control.status === 0 && area && featureRef.current) {
featureRef.current.removeLayer(area);
setArea(null);
setBound(null);
}
}, [control.status, area]);
useEffect(() => {
if (control.status === 2 && bound && featureRef.current) {
setArea(bound);
featureRef.current.addLayer(bound);
bound.editing.enable();
safeFlyToBounds(bound);
bindEditEvent(bound);
}
}, [control.status, bound]);
useEffect(() => {
if (!featureRef.current) return;
L.drawLocal.draw.handlers.polygon.tooltip.start = "برای شروع ترسیم محدوده، روی نقشه کلیک کنید";
L.drawLocal.draw.handlers.polygon.tooltip.cont = "برای ادامه ترسیم، نقاط بعدی را کلیک کنید";
L.drawLocal.draw.handlers.polygon.tooltip.end = "برای بستن محدوده، روی نقطه‌ی شروع کلیک کنید";
const drawControl = new L.Control.Draw({
draw: {
polygon: {
shapeOptions: {
color: "#3388ff",
weight: 3,
clickable: true,
},
},
polyline: false,
rectangle: false,
circle: false,
marker: false,
circlemarker: false,
},
edit: {
featureGroup: featureRef.current,
edit: true,
remove: false,
},
});
drawControlRef.current = drawControl;
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, handlerCreatedBound);
return () => {
map.off(L.Draw.Event.CREATED, handlerCreatedBound);
map.removeControl(drawControl);
};
}, [map, handlerCreatedBound]);
return <FeatureGroup ref={featureRef} />;
};
export default DrawBound;

View File

@@ -0,0 +1,116 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import DrawBound from "./DrawBound";
import { Delete, Edit, Route } from "@mui/icons-material";
import { useReducer } from "react";
const statusType = [
{
id: 0,
message: "برای آغاز ترسیم محدوده، کلیک کنید!",
buttons: [
{
label: "شروع ترسیم محدوده",
key: "start",
color: "primary",
icon: <Route />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 1 });
},
},
],
},
{
id: 1,
message: "محدوده‌ی موردنظر را با کلیک روی نقشه مشخص کنید. برای اتمام، روی نقطه‌ی ابتدایی کلیک کنید!",
buttons: [],
},
{
id: 2,
message: "برای اصلاح محدوده، گوشه‌ها را بکشید. برای ترسیم دوباره، آن را حذف کنید",
buttons: [
{
label: "حذف",
key: "end",
color: "error",
icon: <Delete />,
onclick: (controlDispach) => {
controlDispach({ type: "SET_STATUS", status: 0 });
},
},
],
},
];
const createInitialState = (bound) => {
if (bound) {
return { status: 2 };
}
return { status: 0 };
};
const reducer = (state, action) => {
switch (action.type) {
case "SET_STATUS":
return { ...state, status: action.status };
default:
return state;
}
};
const MapControl = ({ bound, setBound }) => {
const [control, controlDispach] = useReducer(reducer, bound, createInitialState);
return (
<>
<DrawBound bound={bound} setBound={setBound} control={control} controlDispach={controlDispach} />
<Stack
direction={"row"}
justifyContent={"center"}
sx={{ position: "absolute", left: 0, top: 0, zIndex: 2000, width: "100%" }}
>
<Box
sx={{
background: (theme) => theme.palette.info.main,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
py: 0.5,
px: 2,
}}
>
<Typography align="center" component={"div"} variant="caption" color={"#fff"}>
{statusType.find((st) => st.id == control.status).message}
</Typography>
</Box>
</Stack>
<Box
sx={{
position: "absolute",
left: 0,
bottom: 0,
zIndex: 2000,
width: "100%",
py: 1,
background: "linear-gradient(to top, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0))",
}}
>
<Stack direction={"row"} justifyContent={"center"} spacing={2}>
{statusType
.find((st) => st.id == control.status)
.buttons.map((button) => (
<Button
size="small"
key={button.key}
variant="contained"
onClick={() => button.onclick(controlDispach)}
color={button.color}
startIcon={button.icon}
>
{button.label}
</Button>
))}
</Stack>
</Box>
</>
);
};
export default MapControl;

View File

@@ -0,0 +1,39 @@
import MapLoading from "@/core/components/MapLayer/Loading";
import { Box, Button, DialogActions, DialogContent } from "@mui/material";
import dynamic from "next/dynamic";
import MapControl from "./MapControl";
import { useCallback, useState } from "react";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
const [bound, setBound] = useState(allData.bound);
const handleNext = useCallback(() => {
setAllData({ bound: bound });
setTabState((s) => s + 1);
}, [bound]);
return (
<>
<DialogContent>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<MapControl bound={bound} setBound={setBound} />
</MapLayer>
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
</Button>
</DialogActions>
</>
);
};
export default Area;

View File

@@ -0,0 +1,93 @@
import MuiDatePicker from "@/core/components/MuiDatePicker";
import MuiTimePicker from "@/core/components/MuiTimePicker";
import { Grid } from "@mui/material";
import { Controller, useWatch } from "react-hook-form";
const MissionDates = ({ control }) => {
const type = useWatch({ control, name: "type" });
return (
<>
<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}
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
label={"تاریخ شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
/>
</Grid>
{type == 2 && (
<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}
placeholder={"زمان شروع ماموریت را وارد کنید"}
label={"زمان شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
/>
</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}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
/>
</Grid>
{type == 2 && (
<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}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
/>
</Grid>
)}
</>
);
};
export default MissionDates;

View File

@@ -0,0 +1,137 @@
import PersianTextField from "@/core/components/PersianTextField";
import SelectBox from "@/core/components/SelectBox";
import StyledForm from "@/core/components/StyledForm";
import { yupResolver } from "@hookform/resolvers/yup";
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 MissionDates from "./MissionDates";
import moment from "jalali-moment";
const validationSchema = object({
type: string().required("نوع ماموریت را مشخص کنید!"),
start_date: string().required("تاریخ شروع ماموریت را مشخص کنید!"),
start_time: string().when("type", {
is: "2",
then: (schema) => schema.required("زمان شروع ماموریت را مشخص کنید!"),
otherwise: (schema) => schema.notRequired(),
}),
end_date: string().required("تاریخ پایان ماموریت را مشخص کنید!"),
end_time: string().when("type", {
is: "2",
then: (schema) => schema.required("زمان شروع ماموریت را مشخص کنید!"),
otherwise: (schema) => schema.notRequired(),
}),
end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"),
});
const GetItemInfo = ({ types, regions, allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = {
type: allData.type,
start_date: allData.start_date,
start_time: allData.start_time ? moment(allData.start_time).toDate() : null,
end_date: allData.end_date,
end_time: allData.end_time ? moment(allData.end_time).toDate() : null,
end_point: allData.end_point,
region: allData.region,
};
const { control, handleSubmit } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
});
const handleNext = (data) => {
setAllData(data);
setTabState((s) => s + 1);
};
return (
<StyledForm onSubmit={handleSubmit(handleNext)}>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"type"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="نوع ماموریت"
selectors={types}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"region"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="محدوده ماموریت"
selectors={regions}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
/>
)}
/>
</Grid>
<MissionDates control={control} />
<Grid item xs={12}>
<Controller
control={control}
name={"end_point"}
render={({ field, fieldState: { error } }) => {
return (
<PersianTextField
value={field.value}
variant="outlined"
name="end_point"
onChange={field.onChange}
label="مقصد"
placeholder={"مقصد را وارد کنید"}
fullWidth
size="small"
error={error}
helperText={error?.message}
InputLabelProps={{ shrink: true }}
/>
);
}}
/>
</Grid>
</Grid>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button
onClick={handlePrev}
variant="outlined"
color="secondary"
size="large"
startIcon={<ExitToApp />}
>
{"بستن"}
</Button>
<Button variant="contained" size="large" type="submit">
مرحله بعد
</Button>
</DialogActions>
</StyledForm>
);
};
export default GetItemInfo;

View File

@@ -0,0 +1,63 @@
import RahdarCode from "@/core/components/RahdarCode";
import StyledForm from "@/core/components/StyledForm";
import { yupResolver } from "@hookform/resolvers/yup";
import { Button, Stack } from "@mui/material";
import { Controller, useForm } from "react-hook-form";
import { object } from "yup";
const schema = object().shape({
rahdar: object().required("راهدار الزامی است."),
});
const RahdaranForm = ({ setRahdaran }) => {
const defaultValues = {
rahdar: null,
};
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, isValid },
} = useForm({ defaultValues, resolver: yupResolver(schema), mode: "all" });
const submit = (data) => {
setRahdaran((prev) => {
const alreadyExists = prev.some((r) => r.id === data.rahdar.id);
if (alreadyExists) return prev;
return [...prev, data.rahdar];
});
reset();
};
return (
<StyledForm onSubmit={handleSubmit(submit)}>
<Stack direction={{ xs: "column", sm: "row" }} justifyContent={"center"} spacing={2}>
<Stack sx={{ minWidth: "250px" }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<RahdarCode
rahdarsCode={field.value}
setRahdarsCode={(value) => field.onChange(value)}
error={error}
multiple={false}
/>
);
}}
name={"rahdar"}
/>
</Stack>
<Button
variant="contained"
type="submit"
sx={{ textWrap: "nowrap", px: 3 }}
disabled={isSubmitting || !isValid}
>
افزودن راهدار
</Button>
</Stack>
</StyledForm>
);
};
export default RahdaranForm;

View File

@@ -0,0 +1,56 @@
import { AccountCircle, Delete } from "@mui/icons-material";
import { Card, Collapse, IconButton, Stack, Typography } from "@mui/material";
import { TransitionGroup } from "react-transition-group";
const RahdaranList = ({ rahdaran, setRahdaran }) => {
const handleRemove = (index) => {
setRahdaran((prev) => prev.filter((_, i) => i !== index));
};
return (
<Stack>
<TransitionGroup>
{rahdaran.map((rahdar, index) => (
<Collapse key={rahdar.code}>
<Card
elevation={0}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
border: 1,
borderColor: "divider",
px: 1,
py: 1,
my: 1,
}}
>
<Stack direction={"row"} spacing={1}>
<AccountCircle color="primary" sx={{ width: "32px", height: "32px" }} />
<Stack>
<Stack direction={"row"} spacing={0.5}>
<Typography variant="caption">نام و نام خانوادگی:</Typography>
<Typography variant="body2">{rahdar.name}</Typography>
</Stack>
<Stack direction={"row"} spacing={0.5}>
<Typography variant="caption">کد راهدار:</Typography>
<Typography variant="body2">{rahdar.code}</Typography>
</Stack>
</Stack>
</Stack>
<IconButton color="error" onClick={() => handleRemove(index)}>
<Delete />
</IconButton>
</Card>
</Collapse>
))}
{rahdaran.length == 0 && (
<Collapse>
<Typography align="center">راهداری ثبت نشده است</Typography>
</Collapse>
)}
</TransitionGroup>
</Stack>
);
};
export default RahdaranList;

View File

@@ -0,0 +1,36 @@
import { Box, Button, Chip, DialogActions, DialogContent, Divider } from "@mui/material";
import { useCallback, useState } from "react";
import RahdaranForm from "./Form";
import RahdaranList from "./List";
const Rahdaran = ({ allData, setAllData, setTabState, handlePrev }) => {
const [rahdaran, setRahdaran] = useState(allData.rahdaran);
const handleNext = useCallback(() => {
setAllData({ rahdaran: rahdaran });
setTabState((s) => s + 1);
}, [rahdaran]);
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<RahdaranForm setRahdaran={setRahdaran} />
<Divider sx={{ my: 2 }}>
<Chip label="لیست راهداران" />
</Divider>
<RahdaranList rahdaran={rahdaran} setRahdaran={setRahdaran} />
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={rahdaran.length == 0} onClick={handleNext}>
{rahdaran.length == 0 ? "ثبت حداقل یک راهدار" : "مرحله بعد"}
</Button>
</DialogActions>
</>
);
};
export default Rahdaran;

View File

@@ -0,0 +1,89 @@
import LtrTextField from "@/core/components/LtrTextField";
import SelectBox from "@/core/components/SelectBox";
import StyledForm from "@/core/components/StyledForm";
import { machineType } from "@/core/utils/machineTypes";
import { yupResolver } from "@hookform/resolvers/yup";
import { Add } from "@mui/icons-material";
import { Button, Grid, Stack } from "@mui/material";
import moment from "jalali-moment";
import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup";
const validationSchema = object({
type: string().required("نوع خودرو را مشخص کنید!"),
count: string().required("تعداد خودرو را مشخص کنید!"),
});
const RequestMachinesForm = ({ requests, setRequests }) => {
const usedTypeIds = requests.map((r) => r.type);
const filteredMachineTypes = machineType.filter((mt) => !usedTypeIds.includes(mt.id));
const defaultValues = {
type: "",
count: "",
};
const { control, handleSubmit, reset } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
});
const submit = (data) => {
setRequests((r) => [...r, { id: moment().valueOf(), ...data }]);
reset();
};
return (
<StyledForm onSubmit={handleSubmit(submit)}>
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} sm={5}>
<Controller
control={control}
name={"type"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="نوع خودرو"
selectors={filteredMachineTypes}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={5}>
<Controller
control={control}
name={"count"}
render={({ field, fieldState: { error } }) => (
<LtrTextField
fullWidth
{...field}
autoComplete="off"
onChange={(e) => {
if (isNaN(Number(e.target.value))) return;
field.onChange(e.target.value);
}}
size="small"
error={!!error}
helperText={!!error && error.message}
type="tel"
label="تعداد خودرو"
/>
)}
/>
</Grid>
<Grid item xs={12} sm={2}>
<Button variant="outlined" type="submit" fullWidth startIcon={<Add />}>
افزودن
</Button>
</Grid>
</Grid>
</Stack>
</StyledForm>
);
};
export default RequestMachinesForm;

View File

@@ -0,0 +1,59 @@
import { machineType } from "@/core/utils/machineTypes";
import { Delete, FireTruck } from "@mui/icons-material";
import { Card, Collapse, IconButton, Stack, Typography } from "@mui/material";
import { TransitionGroup } from "react-transition-group";
const RequestMachinesList = ({ requests, setRequests }) => {
const handleRemove = (index) => {
setRequests((prev) => prev.filter((_, i) => i !== index));
};
return (
<Stack>
<TransitionGroup>
{requests.map((request, index) => (
<Collapse key={request.id}>
<Card
elevation={0}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
border: 1,
borderColor: "divider",
px: 1,
py: 1,
my: 1,
}}
>
<Stack direction={"row"} spacing={1}>
<FireTruck color="primary" sx={{ width: "32px", height: "32px" }} />
<Stack>
<Stack direction={"row"} spacing={0.5}>
<Typography variant="caption">نوع خودرو:</Typography>
<Typography variant="body2">
{machineType.find((t) => t.id == request.type)?.name_fa || ""}
</Typography>
</Stack>
<Stack direction={"row"} spacing={0.5}>
<Typography variant="caption">تعداد:</Typography>
<Typography variant="body2">{request.count}</Typography>
</Stack>
</Stack>
</Stack>
<IconButton color="error" onClick={() => handleRemove(index)}>
<Delete />
</IconButton>
</Card>
</Collapse>
))}
{requests.length == 0 && (
<Collapse>
<Typography align="center">درخواستی ثبت نشده است</Typography>
</Collapse>
)}
</TransitionGroup>
</Stack>
);
};
export default RequestMachinesList;

View File

@@ -0,0 +1,36 @@
import { Box, Button, Chip, DialogActions, DialogContent, Divider } from "@mui/material";
import { useCallback, useState } from "react";
import RequestMachinesForm from "./Form";
import RequestMachinesList from "./List";
const RequestMachines = ({ allData, setAllData, handlePrev, setTabState }) => {
const [requests, setRequests] = useState(allData.requested_machines);
const handleNext = useCallback(() => {
setAllData({ requested_machines: requests });
setTabState((s) => s + 1);
}, [requests]);
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<RequestMachinesForm requests={requests} setRequests={setRequests} />
<Divider sx={{ my: 2 }}>
<Chip label="لیست درخواست خودرو ها" />
</Divider>
<RequestMachinesList requests={requests} setRequests={setRequests} />
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={requests.length == 0} onClick={handleNext}>
{requests.length == 0 ? "ثبت حداقل یک درخواست" : "مرحله بعد"}
</Button>
</DialogActions>
</>
);
};
export default RequestMachines;

View File

@@ -0,0 +1,118 @@
import MapLoading from "@/core/components/MapLayer/Loading";
import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typography } from "@mui/material";
import dynamic from "next/dynamic";
import { useCallback } from "react";
import moment from "jalali-moment";
import ShowBound from "../../../showBound";
import { machineType } from "@/core/utils/machineTypes";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Verify = ({ types, regions, allData, handlePrev, submitForm, submitting }) => {
const handleNext = useCallback(() => {
submitForm(allData);
}, [allData]);
return (
<>
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack spacing={2}>
<Box sx={{ width: "100%", height: "200px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={allData.bound} />
</MapLayer>
</Box>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />
</Divider>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">نوع ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={types.find((t) => t.id == allData.type).name_fa} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">محدوده ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={regions.find((r) => r.id == allData.region).name_fa} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
{allData.type == 2 ? (
<Chip
size="small"
label={`${moment(allData.start_time).format("HH:mm")} | ${moment(allData.start_date).locale("fa").format("YYYY/MM/DD")}`}
/>
) : (
<Chip
size="small"
label={moment(allData.start_date).locale("fa").format("YYYY/MM/DD")}
/>
)}
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ پایان ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
{allData.type == 2 ? (
<Chip
size="small"
label={`${moment(allData.end_time).format("HH:mm")} | ${moment(allData.end_date).locale("fa").format("YYYY/MM/DD")}`}
/>
) : (
<Chip
size="small"
label={moment(allData.end_date).locale("fa").format("YYYY/MM/DD")}
/>
)}
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">مقصد</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={allData.end_point} />
</Stack>
</Stack>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="خودرو ها" />
</Divider>
{allData.requested_machines.map((request) => (
<Stack key={request.id} direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">
{machineType.find((mt) => mt.id == request.type).name_fa}
</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={request.count} />
</Stack>
))}
</Stack>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="راهداران" />
</Divider>
{allData.rahdaran.map((rahdar) => (
<Stack key={rahdar.id} direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">{rahdar.name}</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={rahdar.code} />
</Stack>
))}
</Stack>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button onClick={handlePrev} variant="outlined" color="secondary" disabled={submitting} size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={submitting} onClick={handleNext}>
تایید و ثبت ماموریت
</Button>
</DialogActions>
</>
);
};
export default Verify;

View File

@@ -0,0 +1,133 @@
import { Engineering, InsertDriveFile, Route, TaxiAlert, Verified } from "@mui/icons-material";
import { Box, Tab, Tabs } from "@mui/material";
import { useReducer, useState } from "react";
import GetItemInfo from "./GetItemInfo";
import RequestMachines from "./RequestMachines";
import Rahdaran from "./Rahdaran";
import Area from "./Area";
import Verify from "./Verify";
const types = [
{ id: 1, name_fa: "روزانه" },
{ id: 2, name_fa: "ساعتی" },
];
const regions = [
{ id: 1, name_fa: "داخل شهر" },
{ id: 2, name_fa: "بیرون شهر - داخل محدوده" },
{ id: 3, name_fa: "بیرون شهر - خارج محدوده" },
];
function TabPanel(props) {
const { children, value, index } = props;
return (
<div role="tabpanel" hidden={value !== index}>
{value === index && <Box>{children}</Box>}
</div>
);
}
const reducer = (state, action) => {
switch (action.type) {
case "changeData":
return { ...state, ...action.data };
default:
return state;
}
};
const CreateForm = ({ defaultValues, submitForm, setOpen, submitting }) => {
const [allData, dispatch] = useReducer(reducer, defaultValues);
const [tabState, setTabState] = useState(0);
const handleClose = () => {
setOpen(false);
};
const handleChangeTab = (event, newValue) => {
setTabState(newValue);
};
const handlePrev = () => {
if (tabState === 0) {
handleClose();
} else {
setTabState((t) => t - 1);
}
};
return (
<>
<Tabs
allowScrollButtonsMobile
value={tabState}
onChange={handleChangeTab}
variant={"fullWidth"}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
backgroundColor: "#efefef",
}}
>
<Tab icon={<InsertDriveFile />} label="مشخصات" />
<Tab disabled={tabState < 1} icon={<Route />} label="محدوده" />
<Tab disabled={tabState < 2} icon={<TaxiAlert />} label="خودرو ها" />
<Tab disabled={tabState < 3} icon={<Engineering />} label="راهداران" />
<Tab disabled={tabState < 4} icon={<Verified />} label="بررسی نهایی" />
</Tabs>
<TabPanel value={tabState} index={0}>
<GetItemInfo
types={types}
regions={regions}
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={1}>
<Area
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={2}>
<RequestMachines
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={3}>
<Rahdaran
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={4}>
<Verify
types={types}
regions={regions}
allData={allData}
handlePrev={handlePrev}
submitForm={submitForm}
submitting={submitting}
/>
</TabPanel>
</>
);
};
export default CreateForm;

View File

@@ -0,0 +1,111 @@
import { AddCircle, Close } from "@mui/icons-material";
import { Button, Dialog, IconButton, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import CreateForm from "./Form";
import useRequest from "@/lib/hooks/useRequest";
import { REQUEST_MISSION } from "@/core/utils/routes";
import moment from "jalali-moment";
const Create = ({ mutate }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const submitForm = async (result) => {
setSubmitting(true);
const bound = result.bound.getLatLngs();
let area = bound.map((ring) => ring.map((latlng) => [latlng.lat, latlng.lng]));
await requestServer(REQUEST_MISSION, "post", {
data: {
area: area[0],
rahdaran: result.rahdaran.map((r) => r.id),
requested_machines: result.requested_machines,
type: result.type,
zone: result.region,
end_point: result.end_point,
...(result.type == 2
? {
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
}
: {
start_date: result.start_date,
end_date: result.end_date,
}),
},
hasSidebarUpdate: true,
})
.then((response) => {
mutate();
setOpen(false);
})
.catch((error) => {})
.finally(() => {
setSubmitting(false);
});
};
return (
<>
{isMobile ? (
<IconButton aria-label="ثبت ماموریت" color="primary" onClick={handleOpen}>
<AddCircle sx={{ fontSize: "25px" }} />
</IconButton>
) : (
<Button
size={"small"}
variant="contained"
color="primary"
startIcon={<AddCircle />}
onClick={handleOpen}
>
ثبت ماموریت
</Button>
)}
<Dialog open={open} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpen(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
{open && (
<CreateForm
defaultValues={{
rahdaran: [],
requested_machines: [],
bound: null,
type: "",
start_date: "",
start_time: null,
end_date: "",
end_time: null,
end_point: "",
region: "",
}}
submitForm={submitForm}
submitting={submitting}
open={open}
setOpen={setOpen}
mutate={mutate}
/>
)}
</Dialog>
</>
);
};
export default Create;

View File

@@ -0,0 +1,55 @@
import MapLayer from "@/core/components/MapLayer";
import { Close, RemoveRedEye } from "@mui/icons-material";
import { Box, Dialog, DialogContent, DialogTitle, IconButton, Tooltip, Typography } from "@mui/material";
import { useMemo, useState } from "react";
import ShowBound from "../showBound";
import L from "leaflet";
const ShowArea = ({ area }) => {
const bound = useMemo(() => L.polygon([...area]), [area]);
const [openAreaDialog, setOpenAreaDialog] = useState(false);
return (
<>
<Tooltip title="محدوده">
<IconButton color="primary" onClick={() => setOpenAreaDialog(true)}>
<RemoveRedEye />
</IconButton>
</Tooltip>
<Dialog
open={openAreaDialog}
onClose={() => setOpenAreaDialog(false)}
PaperProps={{
sx: {
transition: "all .3s",
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
borderRadius: 2,
padding: 1,
},
}}
maxWidth="sm"
fullWidth
dir="rtl"
>
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
محدوده
</Typography>
<IconButton onClick={() => setOpenAreaDialog(false)} size="small">
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ flex: 1 }}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
</Box>
</Box>
</DialogContent>
</Dialog>
</>
);
};
export default ShowArea;

View File

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

View File

@@ -0,0 +1,136 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_ROAD_MISSIONS_OPERATOR_LIST } from "@/core/utils/routes";
import { Box, Stack } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
import ShowArea from "./Actions/showArea";
import RowActions from "./RowActions";
import Toolbar from "./Toolbar";
const typeOptions = [
{ value: 1, label: "روزانه" },
{ value: 2, label: "ساعتی" },
];
const OperatorList = () => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "type",
header: "نوع",
id: "type",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
sortDescFirst: true,
grow: false,
size: 100,
columnSelectOption: () => {
return typeOptions.map((type) => ({
value: type.value,
label: type.label,
}));
},
Cell: ({ row }) => row.original.type_fa,
},
{
accessorFn: (row) => moment(row.start_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ شروع",
id: "start_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorFn: (row) => moment(row.end_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ پایان",
id: "end_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorKey: "end_point",
header: "مقصد",
id: "end_point",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
},
{
header: "محدوده",
id: "area",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
py: 0,
"&:first-of-type": {
borderLeft: "unset",
},
},
},
Cell: ({ row }) =>
row.original.area ? (
<Stack alignItems={"center"} justifyContent={"center"}>
<ShowArea area={row.original.area} />
</Stack>
) : (
"-"
),
},
{
accessorKey: "state_name",
header: "وضعیت",
id: "state_name",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
},
],
[]
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_ROAD_MISSIONS_OPERATOR_LIST}
page_name={"roadMissionsOperator"}
table_name={"roadMissionsOperatorList"}
TableToolbar={Toolbar}
enableRowActions
positionActionsColumn={"first"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default OperatorList;

View File

@@ -0,0 +1,39 @@
import { DELETE_REQUEST_MISSION } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
import { useState } from "react";
const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const handleClick = () => {
setSubmitting(true);
requestServer(`${DELETE_REQUEST_MISSION}/${rowId}`, "delete")
.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,34 @@
import DeleteIcon from "@mui/icons-material/Delete";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import DeleteContent from "./DeleteContent";
const Delete = ({ rowId, mutate }) => {
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>
{openDeleteDialog && (
<DeleteContent rowId={rowId} mutate={mutate} setOpenDeleteDialog={setOpenDeleteDialog} />
)}
</Dialog>
</>
);
};
export default Delete;

View File

@@ -0,0 +1,92 @@
import { useEffect, useMemo, useState } from "react";
import useRequest from "@/lib/hooks/useRequest";
import DialogLoading from "@/core/components/DialogLoading";
import CreateForm from "../../../Actions/Create/Form";
import moment from "jalali-moment";
import L from "leaflet";
import { GET_RAHDARAN_BY_ID, UPDATE_REQUEST_MISSION } from "@/core/utils/routes";
const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
const bound = useMemo(() => L.polygon([...row.original.area]), [row.original.area]);
const [submitting, setSubmitting] = useState(false);
const [rahdaran, setRahdaran] = useState(null);
const [rahdaranLoading, setRahdaranLoading] = useState(false);
const requestServer = useRequest();
useEffect(() => {
setRahdaranLoading(true);
requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get")
.then((response) => {
setRahdaran(response.data.data);
setRahdaranLoading(false);
})
.catch((e) => {
setRahdaranLoading(false);
});
}, [row.original.id]);
const submitForm = async (result) => {
setSubmitting(true);
const bound = result.bound.getLatLngs();
let area = bound.map((ring) => ring.map((latlng) => [latlng.lat, latlng.lng]));
await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", {
data: {
area: area[0],
rahdaran: result.rahdaran.map((r) => r.id),
requested_machines: result.requested_machines,
type: result.type,
zone: result.region,
end_point: result.end_point,
...(result.type == 2
? {
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
}
: {
start_date: result.start_date,
end_date: result.end_date,
}),
},
hasSidebarUpdate: true,
notificationSuccess: true,
})
.then((response) => {
mutate();
setOpenEditDialog(false);
})
.catch((error) => {})
.finally(() => {
setSubmitting(false);
});
};
return (
<>
{rahdaranLoading ? (
<DialogLoading />
) : (
<CreateForm
defaultValues={{
rahdaran: rahdaran,
requested_machines: row.original.requested_machines,
bound: bound,
type: row.original.type,
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
start_time: moment(row.original.start_date).toDate(),
end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
end_time: moment(row.original.end_date).toDate(),
end_point: row.original.end_point,
region: row.original.zone,
}}
submitForm={submitForm}
submitting={submitting}
open={openEditDialog}
setOpen={setOpenEditDialog}
mutate={mutate}
/>
)}
</>
);
};
export default EditController;

View File

@@ -0,0 +1,48 @@
import { BorderColor, Close } from "@mui/icons-material";
import { Dialog, IconButton, Tooltip } from "@mui/material";
import EditController from "./EditController";
import { useState } from "react";
const Edit = ({ row, mutate }) => {
const [openEditDialog, setOpenEditDialog] = useState(false);
return (
<>
<Tooltip title="ویرایش" arrow placement="right">
<IconButton
color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => {
setOpenEditDialog(true);
}}
>
<BorderColor />
</IconButton>
</Tooltip>
<Dialog open={openEditDialog} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpenEditDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
{openEditDialog && (
<EditController
openEditDialog={openEditDialog}
setOpenEditDialog={setOpenEditDialog}
row={row}
mutate={mutate}
/>
)}
</Dialog>
</>
);
};
export default Edit;

View File

@@ -0,0 +1,17 @@
import { Box } from "@mui/material";
import Edit from "./Edit";
import Delete from "./Delete";
const RowActions = ({ row, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1 }}>
{row.original.state_id == 5 && (
<>
<Edit mutate={mutate} row={row} />
<Delete mutate={mutate} rowId={row.original.id} />
</>
)}
</Box>
);
};
export default RowActions;

View File

@@ -0,0 +1,11 @@
import { Stack } from "@mui/material";
import Create from "./Actions/Create";
const Toolbar = ({ table, filterData, mutate }) => {
return (
<Stack direction={"row"} spacing={1}>
<Create mutate={mutate} />
</Stack>
);
};
export default Toolbar;

View File

@@ -0,0 +1,14 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import OperatorList from "./OperatorList";
const OperatorPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"کارتابل عملیات ماموریت"} />
<OperatorList />
</Stack>
);
};
export default OperatorPage;

View File

@@ -0,0 +1,55 @@
import MapLayer from "@/core/components/MapLayer";
import { Close, RemoveRedEye } from "@mui/icons-material";
import { Box, Dialog, DialogContent, DialogTitle, IconButton, Tooltip, Typography } from "@mui/material";
import { useMemo, useState } from "react";
import ShowBound from "../showBound";
import L from "leaflet";
const ShowArea = ({ area }) => {
const bound = useMemo(() => L.polygon([...area]), [area]);
const [openAreaDialog, setOpenAreaDialog] = useState(false);
return (
<>
<Tooltip title="محدوده">
<IconButton color="primary" onClick={() => setOpenAreaDialog(true)}>
<RemoveRedEye />
</IconButton>
</Tooltip>
<Dialog
open={openAreaDialog}
onClose={() => setOpenAreaDialog(false)}
PaperProps={{
sx: {
transition: "all .3s",
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
borderRadius: 2,
padding: 1,
},
}}
maxWidth="sm"
fullWidth
dir="rtl"
>
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
محدوده
</Typography>
<IconButton onClick={() => setOpenAreaDialog(false)} size="small">
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ flex: 1 }}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={bound} />
</MapLayer>
</Box>
</Box>
</DialogContent>
</Dialog>
</>
);
};
export default ShowArea;

View File

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

View File

@@ -0,0 +1,24 @@
import { Done } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material";
const Allocate = ({ row, request, dispatch, setOpenMachinesDialog }) => {
const handleClick = () => {
dispatch({
type: "CHANGE_MACHINE",
id: request.id,
machine: row.original,
});
setOpenMachinesDialog(false);
};
return (
<>
<Tooltip title="تخصیص" arrow placement="right">
<IconButton color="primary" sx={{ textTransform: "unset", alignSelf: "center" }} onClick={handleClick}>
<Done />
</IconButton>
</Tooltip>
</>
);
};
export default Allocate;

View File

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

View File

@@ -0,0 +1,61 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { Box } from "@mui/material";
import { useMemo } from "react";
import RowActions from "./RowActions";
import { GET_MACHINES_TABLE_LIST } from "@/core/utils/routes";
const MachinesList = ({ request, dispatch, setOpenMachinesDialog }) => {
const columns = useMemo(
() => [
{
accessorKey: "machine_code",
header: "کد خودرو",
id: "machine_code",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "car_name",
header: "نام خودرو",
id: "car_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
[]
);
return (
<>
<Box sx={{ mt: 3, p: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_MACHINES_TABLE_LIST}
page_name={"roadMissionsTransportation"}
table_name={"roadMissionsTransportationMachinesList"}
enableRowActions
positionActionsColumn={"first"}
RowActions={(props) => (
<RowActions
{...props}
request={request}
dispatch={dispatch}
setOpenMachinesDialog={setOpenMachinesDialog}
/>
)}
/>
</Box>
</>
);
};
export default MachinesList;

View File

@@ -0,0 +1,40 @@
import { Close, Edit } from "@mui/icons-material";
import { Button, Dialog, IconButton } from "@mui/material";
import { useState } from "react";
import MachinesList from "./MachineList";
const MachinesDialog = ({ request, dispatch, mode }) => {
const [openMachinesDialog, setOpenMachinesDialog] = useState(false);
return (
<>
{mode == "edit" ? (
<IconButton onClick={() => setOpenMachinesDialog(true)} size="small">
<Edit />
</IconButton>
) : (
<Button onClick={() => setOpenMachinesDialog(true)} variant="outlined" size="small">
انتخاب خودرو
</Button>
)}
<Dialog open={openMachinesDialog} fullWidth maxWidth="md">
<IconButton
aria-label="close"
onClick={() => setOpenMachinesDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
{openMachinesDialog && (
<MachinesList request={request} dispatch={dispatch} setOpenMachinesDialog={setOpenMachinesDialog} />
)}
</Dialog>
</>
);
};
export default MachinesDialog;

View File

@@ -0,0 +1,84 @@
import { DEALLOCATE_REQUEST_MISSION } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup";
import { Button, DialogActions, DialogContent, Stack, TextField } from "@mui/material";
import { useForm } from "react-hook-form";
import { object, string } from "yup";
const RejectForm = ({ rowId, mutate, setOpenAllocationDialog, setOpenRejectDialog }) => {
const requestServer = useRequest({ notificationSuccess: true });
const validationSchema = object().shape({
description: string().required("لطفاً توضیحات را وارد کنید."),
});
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm({
defaultValues: {
description: "",
},
resolver: yupResolver(validationSchema),
mode: "all",
});
const onSubmit = async (data) => {
const formData = new FormData();
formData.append("description", data.description);
await requestServer(`${DEALLOCATE_REQUEST_MISSION}/${rowId}`, "post", {
data: formData,
hasSidebarUpdate: true,
})
.then(() => {
mutate();
setOpenRejectDialog(false);
setOpenAllocationDialog(false);
})
.catch(() => {})
.finally(() => {
reset();
});
};
return (
<>
<DialogContent>
<Stack spacing={2}>
<Stack>
<TextField
{...register("description")}
multiline
rows={8}
label="توضیحات و دلایل رد درخواست"
placeholder="لطفاً علت رد درخواست خود را توضیح دهید"
error={!!errors.description}
helperText={errors.description?.message}
fullWidth
variant="outlined"
sx={{ mt: 1 }}
InputLabelProps={{ shrink: true }}
/>
</Stack>
</Stack>
</DialogContent>
<DialogActions>
<Button
onClick={() => setOpenRejectDialog(false)}
variant="outlined"
color="secondary"
disabled={isSubmitting}
autoFocus
>
بستن
</Button>
<Button onClick={handleSubmit(onSubmit)} variant="contained" color="primary" disabled={isSubmitting}>
{isSubmitting ? "درحال ثبت رد درخواست..." : "ثبت رد درخواست"}
</Button>
</DialogActions>
</>
);
};
export default RejectForm;

View File

@@ -0,0 +1,40 @@
import { Close } from "@mui/icons-material";
import { Button, Dialog, DialogTitle, IconButton } from "@mui/material";
import { useState } from "react";
import RejectForm from "./Form";
const Reject = ({ isSubmitting, rowId, mutate, setOpenAllocationDialog }) => {
const [openRejectDialog, setOpenRejectDialog] = useState(false);
return (
<>
<Button onClick={() => setOpenRejectDialog(true)} variant="outlined" color="error" disabled={isSubmitting}>
رد درخواست
</Button>
<Dialog open={openRejectDialog} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpenRejectDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
<DialogTitle>رد درخواست</DialogTitle>
{openRejectDialog && (
<RejectForm
rowId={rowId}
mutate={mutate}
setOpenAllocationDialog={setOpenAllocationDialog}
setOpenRejectDialog={setOpenRejectDialog}
/>
)}
</Dialog>
</>
);
};
export default Reject;

View File

@@ -0,0 +1,121 @@
import { machineType } from "@/core/utils/machineTypes";
import { Alert, Box, Button, Chip, DialogActions, DialogContent, Divider, Typography } from "@mui/material";
import { Stack } from "@mui/system";
import { useReducer, useState } from "react";
import MachinesDialog from "./MachinesDialog";
import useRequest from "@/lib/hooks/useRequest";
import { ALLOCATE_REQUEST_MISSION } from "@/core/utils/routes";
import Reject from "./Reject";
const initValue = (requested_machines) => {
let arr = [];
let counter = 0;
for (let i = 0; i < requested_machines.length; i++) {
const request = requested_machines[i];
for (let j = 0; j < request.count; j++) {
counter++;
arr.push({ id: counter, type: request.type, machine: null, position: j + 1 });
}
}
return arr;
};
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE_MACHINE":
return state.map((s) => (s.id == action.id ? { ...s, machine: action.machine } : s));
default:
return state;
}
};
const AllocationForm = ({ rowId, mutate, setOpenAllocationDialog, requested_machines }) => {
const requestServer = useRequest({ notificationSuccess: true });
const [requests, dispatch] = useReducer(reducer, requested_machines, initValue);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (_requests) => {
setIsSubmitting(true);
await requestServer(`${ALLOCATE_REQUEST_MISSION}/${rowId}`, "post", {
data: {
machines: _requests.map((r) => r.machine.id),
},
hasSidebarUpdate: true,
})
.then((response) => {
mutate();
setOpenAllocationDialog(false);
})
.catch((error) => {})
.finally(() => {
setIsSubmitting(false);
});
};
return (
<>
<DialogContent dividers>
<Stack spacing={2}>
{requests.map((request) => (
<Stack key={request.id} direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">
{machineType.find((mt) => mt.id == request.type).name_fa} {request.position}
</Typography>
<Divider sx={{ flex: 1 }}>
{!request.machine && (
<Chip label="خودرویی انتخاب نشده" size="small" color="error" variant="outlined" />
)}
</Divider>
{request.machine ? (
<>
<Chip
size="small"
label={`${request.machine.machine_code} | ${request.machine.car_name}`}
/>
<MachinesDialog request={request} dispatch={dispatch} mode={"edit"} />
</>
) : (
<MachinesDialog request={request} dispatch={dispatch} />
)}
</Stack>
))}
{requests.some((r) => !r.machine) && (
<Alert color="warning" icon={false}>
برای تایید و تخصیص باید همه خودرو ها انتخاب شده باشند!
</Alert>
)}
</Stack>
</DialogContent>
<DialogActions sx={{ display: "flex", justifyContent: "space-between" }}>
<Stack direction={"row"} spacing={1}>
<Button
onClick={() => setOpenAllocationDialog(false)}
variant="outlined"
color="primary"
disabled={isSubmitting}
>
بستن
</Button>
<Reject
isSubmitting={isSubmitting}
rowId={rowId}
mutate={mutate}
setOpenAllocationDialog={setOpenAllocationDialog}
/>
</Stack>
<Box>
<Button
onClick={() => handleSubmit(requests)}
variant="contained"
color="primary"
disabled={requests.some((r) => !r.machine) || isSubmitting}
autoFocus
>
تایید و تخصیص
</Button>
</Box>
</DialogActions>
</>
);
};
export default AllocationForm;

View File

@@ -0,0 +1,49 @@
import { Close, LocalShipping } from "@mui/icons-material";
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import AllocationForm from "./Form";
const Allocation = ({ row, mutate }) => {
const [openAllocationDialog, setOpenAllocationDialog] = useState(false);
return (
<>
<Tooltip title="لیست خودرو های درخواستی" arrow placement="right">
<IconButton
color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => {
setOpenAllocationDialog(true);
}}
>
<LocalShipping />
</IconButton>
</Tooltip>
<Dialog open={openAllocationDialog} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpenAllocationDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
<DialogTitle>لیست خودرو های درخواستی</DialogTitle>
{openAllocationDialog && (
<AllocationForm
requested_machines={row.original.requested_machines}
rowId={row.original.id}
mutate={mutate}
setOpenAllocationDialog={setOpenAllocationDialog}
/>
)}
</Dialog>
</>
);
};
export default Allocation;

View File

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

View File

@@ -0,0 +1,125 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_ROAD_MISSIONS_TRANSPORTATION_LIST } from "@/core/utils/routes";
import { Box, Stack } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
import ShowArea from "./Actions/showArea";
import RowActions from "./RowActions";
const TransportationList = () => {
const typeOptions = [
{ value: 1, label: "روزانه" },
{ value: 2, label: "ساعتی" },
];
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "type",
header: "نوع",
id: "type",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
sortDescFirst: true,
grow: false,
size: 100,
columnSelectOption: () => {
return typeOptions.map((type) => ({
value: type.value,
label: type.label,
}));
},
Cell: ({ row }) => row.original.type_fa,
},
{
accessorFn: (row) => moment(row.start_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ شروع",
id: "start_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorFn: (row) => moment(row.end_date).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ پایان",
id: "end_date",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
{
accessorKey: "end_point",
header: "مقصد",
id: "end_point",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
},
{
header: "محدوده",
id: "area",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
grow: false,
size: 100,
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
py: 0,
"&:first-of-type": {
borderLeft: "unset",
},
},
},
Cell: ({ row }) =>
row.original.area ? (
<Stack alignItems={"center"} justifyContent={"center"}>
<ShowArea area={row.original.area} />
</Stack>
) : (
"-"
),
},
],
[]
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_ROAD_MISSIONS_TRANSPORTATION_LIST}
page_name={"roadMissionsTransportation"}
table_name={"roadMissionsTransportationList"}
enableRowActions
positionActionsColumn={"first"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default TransportationList;

View File

@@ -0,0 +1,14 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import TransportationList from "./TransportationList";
const TransportationPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"کارتابل عملیات ماموریت"} />
<TransportationList />
</Stack>
);
};
export default TransportationPage;

View File

@@ -1,16 +1,19 @@
import React from "react"; import ClearIcon from "@mui/icons-material/Clear";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali"; import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
import { faIR } from "@mui/x-date-pickers/locales"; import { faIR } from "@mui/x-date-pickers/locales";
import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import ClearIcon from "@mui/icons-material/Clear"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { parseISO } from "date-fns";
const parseIfString = (val) =>
val instanceof Date ? val : typeof val === "string" && val !== "" ? parseISO(val) : null;
function MuiTimePicker({ value, setFieldValue, name, minTime, maxTime, helperText, placeholder, error, label }) { function MuiTimePicker({ value, setFieldValue, name, minTime, maxTime, helperText, placeholder, error, label }) {
// Ensure value, minTime, maxTime are Date objects // Ensure value, minTime, maxTime are Date objects
const parsedValue = value || null; const parsedValue = parseIfString(value);
const parsedMinTime = minTime || null; const parsedMinTime = parseIfString(minTime);
const parsedMaxTime = maxTime || null; const parsedMaxTime = parseIfString(maxTime);
return ( return (
<> <>

View File

@@ -0,0 +1,4 @@
export const machineType = [
{ id: "grader", name_fa: "گریدر" },
{ id: "loder", name_fa: "لودر" },
];

View File

@@ -22,6 +22,7 @@ import LineAxisIcon from "@mui/icons-material/LineAxis";
import ScienceIcon from "@mui/icons-material/Science"; import ScienceIcon from "@mui/icons-material/Science";
import DriveEtaIcon from "@mui/icons-material/DriveEta"; import DriveEtaIcon from "@mui/icons-material/DriveEta";
import BiotechIcon from "@mui/icons-material/Biotech"; import BiotechIcon from "@mui/icons-material/Biotech";
import { Mediation } from "@mui/icons-material";
export const pageMenuDev = [ export const pageMenuDev = [
{ {
@@ -89,6 +90,36 @@ export const pageMenuDev = [
}, },
], ],
}, },
{
id: "roadMissionsManagemnet",
label: "ماموریت ها",
type: "menu",
icon: <Mediation sx={{ width: "inherit", height: "inherit" }} />,
hasSubitems: true,
Subitems: [
{
id: "roadMissionsManagemnetRequests",
label: "کارتابل",
type: "page",
route: "/dashboard/road-missions/operator",
permissions: ["all"],
},
{
id: "roadMissionsManagemnetNaqlie",
label: "نقلیه",
type: "page",
route: "/dashboard/road-missions/transportation",
permissions: ["all"],
},
{
id: "roadMissionsManagemnetControl",
label: "کنترل",
type: "page",
route: "/dashboard/road-missions/control",
permissions: ["all"],
},
],
},
{ {
id: "roadItemManagment", id: "roadItemManagment",
label: "فعالیت های روزانه", label: "فعالیت های روزانه",

View File

@@ -176,3 +176,18 @@ export const GET_TOLL_HOUSE_DETAILS = api + "/api/v3/road_maintenance_station";
export const DELETE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station"; export const DELETE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station";
export const UPDATE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station"; export const UPDATE_TOLL_HOUSE_ITEM = api + "/api/v3/road_maintenance_station";
export const GET_TOLL_HOUSE_IMAGES = api + "/api/v3/road_maintenance_station/images"; export const GET_TOLL_HOUSE_IMAGES = api + "/api/v3/road_maintenance_station/images";
// road missions
export const GET_ROAD_MISSIONS_OPERATOR_LIST = api + "/api/v3/missions/request_portal";
export const GET_ROAD_MISSIONS_TRANSPORTATION_LIST = api + "/api/v3/missions/transportation_unit";
export const GET_ROAD_MISSIONS_CONTROL_LIST = api + "/api/v3/missions/control_unit";
export const REQUEST_MISSION = api + "/api/v3/missions/request_portal";
export const UPDATE_REQUEST_MISSION = api + "/api/v3/missions/request_portal";
export const DELETE_REQUEST_MISSION = api + "/api/v3/missions/request_portal";
export const GET_RAHDARAN_BY_ID = api + "/api/v3/missions/details/rahdaran";
export const ALLOCATE_REQUEST_MISSION = api + "/api/v3/missions/transportation_unit/allocate";
export const DEALLOCATE_REQUEST_MISSION = api + "/api/v3/missions/transportation_unit/deallocate";
export const START_MISSION = api + "/api/v3/missions/control_unit/start";
export const FINISH_MISSION = api + "/api/v3/missions/control_unit/finish";
export const GET_MACHINES_TABLE_LIST = api + "/api/v3/cmms_machines";

View File

@@ -118,3 +118,33 @@ export const commonCellBeforeAfterStyles = {
width: "100%", width: "100%",
zIndex: -1, zIndex: -1,
}; };
function closePolygonIfNeeded(coords) {
if (
coords.length > 0 &&
(coords[0][0] !== coords[coords.length - 1][0] || coords[0][1] !== coords[coords.length - 1][1])
) {
coords.push(coords[0]);
}
return coords;
}
export const getClosedPolygonCoordinates = (polygon) => {
const latLngs = polygon.getLatLngs();
// گرفتن حلقه اصلی (فرض بر این است که فقط یک ring وجود دارد)
const ring = Array.isArray(latLngs[0]) ? latLngs[0] : latLngs;
// تبدیل به آرایه ساده [lat, lng]
const coordinates = ring.map((point) => [point.lat, point.lng]);
// بررسی بسته بودن حلقه
const first = coordinates[0];
const last = coordinates[coordinates.length - 1];
if (first[0] !== last[0] || first[1] !== last[1]) {
coordinates.push(first); // بستن حلقه
}
return coordinates;
};