Merge branch 'release/v1.8.3'

This commit is contained in:
AmirHossein Mahmoodi
2025-10-12 09:41:38 +03:30
38 changed files with 2054 additions and 58 deletions

View File

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

View File

@@ -3,7 +3,9 @@ import WithPermission from "@/core/middlewares/withPermission";
export default function page() {
return (
<WithPermission permission_name={["show-rahdaran"]}>
<WithPermission
permission_name={["manage-rahdaran-country", "manage-rahdaran-province", "manage-rahdaran-edareh"]}
>
<RahdaranTablePage />
</WithPermission>
);

View File

@@ -1,8 +1,6 @@
import { Controller } from "react-hook-form";
import MuiDatePicker from "@/core/components/MuiDatePicker";
import React from "react";
import { FormControl, FormHelperText, InputLabel, LinearProgress, MenuItem, Select, Typography } from "@mui/material";
import useProvinces from "@/lib/hooks/useProvince";
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material";
import { Controller } from "react-hook-form";
const SelectProvince = ({ control }) => {
const { provinces, loadingProvinces, errorProvinces } = useProvinces();

View File

@@ -0,0 +1,39 @@
import SelectBox from "@/core/components/SelectBox";
import useEdaratLists from "@/lib/hooks/useEdaratLists";
import { useEffect, useState } from "react";
const CityContent = ({ provinceID, field, fieldState: { error } }) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(provinceID);
const [prevDependency, setPrevDependency] = useState(provinceID);
useEffect(() => {
if (prevDependency === provinceID) return;
field.onChange(null);
setPrevDependency(provinceID);
}, [provinceID]);
return (
<SelectBox
value={
!provinceID
? "dependency"
: errorEdaratList
? "error"
: loadingEdaratList
? "loading"
: field.value || ""
}
label={"اداره"}
selectors={edaratList}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
isLoading={loadingEdaratList}
errorEcured={errorEdaratList}
isDependency={!provinceID}
dependencyLabel={"ابتدا استان را انتخاب کنید"}
/>
);
};
export default CityContent;

View File

@@ -0,0 +1,14 @@
import { Controller, useWatch } from "react-hook-form";
import CityContent from "./CityContent";
const CityController = ({ control }) => {
const provinceID = useWatch({ control, name: "provinceId" });
return (
<Controller
name="edarehShahriId"
control={control}
render={(props) => <CityContent provinceID={provinceID} {...props} />}
/>
);
};
export default CityController;

View File

@@ -3,11 +3,23 @@ import StyledForm from "@/core/components/StyledForm";
import validateNationalCode from "@/core/utils/nationalCodeValidation";
import { yupResolver } from "@hookform/resolvers/yup";
import { Beenhere, ExitToApp } from "@mui/icons-material";
import { Button, DialogActions, DialogContent, Grid, Stack, TextField } from "@mui/material";
import {
Button,
DialogActions,
DialogContent,
Grid,
Stack,
TextField,
ToggleButton,
ToggleButtonGroup,
} from "@mui/material";
import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup";
import LogesticController from "./LogesticController";
const validationSchema = object({
provinceId: string().required("لطفا استان را وارد کنید!!!"),
edarehShahriId: string().required("لطفا اداره را وارد کنید!!!"),
name: string().required("وارد کردن نام و نام خانوادگی الزامیست!"),
code: string()
.required("لطفا کد ملی را وارد کنید!!!")
@@ -37,6 +49,32 @@ const CreateFormContent = ({ setOpen, defaultValues, onSubmitBase }) => {
<StyledForm onSubmit={handleSubmit(handleOnSubmit)}>
<DialogContent dividers>
<Stack spacing={3}>
{/* <Stack justifyContent={"center"} alignItems={"center"}>
<Controller
name="isDriver"
control={control}
render={({ field, fieldState: { error } }) => (
<ToggleButtonGroup
size="small"
value={field.value}
exclusive
onChange={(event, newAlignment) => {
if (newAlignment !== null) {
field.onChange(newAlignment);
}
}}
>
<ToggleButton sx={{ px: 3 }} value="0">
راهدار
</ToggleButton>
<ToggleButton sx={{ px: 3 }} value="1">
راننده
</ToggleButton>
</ToggleButtonGroup>
)}
/>
</Stack> */}
<LogesticController control={control} />
<Stack>
<Grid spacing={2}>
<Grid item xs={12} sm={6}>

View File

@@ -0,0 +1,42 @@
import { Grid, Stack } from "@mui/material";
import CityController from "./CityController";
import useProvinces from "@/lib/hooks/useProvince";
import { Controller, useWatch } from "react-hook-form";
import SelectBox from "@/core/components/SelectBox";
const LogesticController = ({ control }) => {
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const isProvince = useWatch({ control, name: "isProvince" });
return (
<>
{!isProvince ? (
<Stack>
<Controller
name="provinceId"
control={control}
render={({ field, fieldState: { error } }) => {
return (
<SelectBox
value={errorProvinces ? "error" : loadingProvinces ? "loading" : field.value || ""}
label="استان"
selectors={provinces}
schema={{ name: "name_fa", value: "id" }}
error={error}
disabled={isProvince}
onChange={field.onChange}
helperText={error?.message}
isLoading={loadingProvinces}
errorEcured={errorProvinces}
/>
);
}}
/>
</Stack>
) : null}
<Stack>
<CityController control={control} />
</Stack>
</>
);
};
export default LogesticController;

View File

@@ -1,19 +1,32 @@
import { Dialog, DialogTitle, IconButton } from "@mui/material";
import CreateFormContent from "./CreateFormContent";
import CloseIcon from "@mui/icons-material/Close";
import useRequest from "@/lib/hooks/useRequest";
import { CREATE_RAHDARAN_ITEM } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import { usePermissions } from "@/lib/hooks/usePermissions";
import useRequest from "@/lib/hooks/useRequest";
import CloseIcon from "@mui/icons-material/Close";
import { Dialog, DialogTitle, IconButton } from "@mui/material";
import { DialogHeader } from "next/dist/client/components/react-dev-overlay/internal/components/Dialog";
import CreateFormContent from "./CreateFormContent";
const CreateForm = ({ open, setOpen, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const hasCountryPermission = userPermissions?.includes("manage-rahdaran-country");
const defaultValues = {
isDriver: "0",
isProvince: !hasCountryPermission,
provinceId: !hasCountryPermission ? user.province_id : "",
edarehShahriId: "",
name: "",
code: "",
phone_number: "",
};
const onSubmit = async (result) => {
const formData = new FormData();
formData.append("is_driver", result.isDriver);
formData.append("province_id", result.provinceId);
formData.append("edareh_shahri_id", result.edarehShahriId);
formData.append("name", result.name);
formData.append("code", result.code);
formData.append("mobile", result.phone_number);

View File

@@ -8,12 +8,18 @@ const EditController = ({ rowId, mutate, setOpen, row }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultData = {
isDriver: `${row.original?.is_driver}` || "0",
provinceId: row.original?.province_id || "",
edarehShahriId: row.original?.edareh_shahri_id || "",
name: row.original?.name || "",
code: row.original?.code || "",
phone_number: row.original?.mobile || "",
};
const handleSubmit = async (result) => {
const formData = new FormData();
formData.append("is_driver", result.isDriver);
formData.append("province_id", result.provinceId);
formData.append("edareh_shahri_id", result.edarehShahriId);
formData.append("name", result.name);
formData.append("code", result.code);
formData.append("mobile", result.phone_number);

View File

@@ -2,13 +2,57 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_RAHDARAN_TABLE_LIST } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import RowActions from "./RowActions";
import Toolbar from "./Toolbar";
import { useAuth } from "@/lib/contexts/auth";
import useProvinces from "@/lib/hooks/useProvince";
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import useEdaratLists from "@/lib/hooks/useEdaratLists";
import { usePermissions } from "@/lib/hooks/usePermissions";
const RahdaranList = () => {
const columns = useMemo(
() => [
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const hasCountryPermission = userPermissions?.includes("manage-rahdaran-country");
const columns = useMemo(() => {
const provinceColumn = {
accessorKey: "province_id",
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: ({ row }) => <>{row.original.province_name}</>,
};
return [
{
accessorKey: "id",
header: "کد یکتا",
@@ -21,6 +65,64 @@ const RahdaranList = () => {
grow: false,
size: 100,
},
...(hasCountryPermission ? [provinceColumn] : []),
{
header: "اداره",
id: "edareh_shahri_id",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
dependencyId: hasCountryPermission ? "province_id" : null,
grow: false,
size: 120,
ColumnSelectComponent: (props) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
);
const [prevDependency, setPrevDependency] = useState(
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
);
const getColumnSelectOptions = useMemo(() => {
if (hasCountryPermission && props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingEdaratList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorEdaratList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...edaratList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [edaratList, loadingEdaratList, errorEdaratList]);
useEffect(() => {
if (hasCountryPermission) return;
if (prevDependency === props.dependencyFieldValue?.value) return;
props.handleChange({ ...props.filterParameters, value: "" });
setPrevDependency(props.dependencyFieldValue?.value);
}, [props.dependencyFieldValue?.value, hasCountryPermission]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingEdaratList
? "loading"
: props.filterParameters.value
}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ renderedCellValue, row }) => <>{row.original.edareh_shahri_name}</>,
},
{
accessorKey: "name",
header: "نام و نام خانوادگی",
@@ -54,9 +156,8 @@ const RahdaranList = () => {
grow: false,
size: 100,
},
],
[]
);
];
}, []);
return (
<>

View File

@@ -0,0 +1,129 @@
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 safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("fitBounds 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();
safeFitBounds(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 { Delete, Route } from "@mui/icons-material";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useReducer } from "react";
import DrawBound from "./DrawBound";
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 MapControlPolygon = ({ 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 MapControlPolygon;

View File

@@ -0,0 +1,129 @@
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 safeFitBounds = (layer) => {
if (!layer || !map) return;
try {
const latLngs = layer.getLatLngs();
map.fitBounds(latLngs, {
paddingTopLeft: [20, 35],
paddingBottomRight: [20, 55],
});
} catch (e) {
console.warn("fitBounds 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?.polyline?.handler
) {
drawControlRef.current._toolbars.draw._modes.polyline.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();
safeFitBounds(bound);
bindEditEvent(bound);
}
}, [control.status, bound]);
useEffect(() => {
if (!featureRef.current) return;
L.drawLocal.draw.handlers.polyline.tooltip.start = "برای شروع ترسیم مسیر، روی نقشه کلیک کنید";
L.drawLocal.draw.handlers.polyline.tooltip.cont = "برای ادامه ترسیم، نقاط بعدی را کلیک کنید";
L.drawLocal.draw.handlers.polyline.tooltip.end = "برای بستن مسیر، روی نقطه پایانی کلیک کنید";
const drawControl = new L.Control.Draw({
draw: {
polyline: {
shapeOptions: {
color: "#3388ff",
weight: 3,
clickable: true,
},
},
polygon: 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 { Delete, Route } from "@mui/icons-material";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useReducer } from "react";
import DrawBound from "./DrawBound";
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 MapControlPolyline = ({ 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 MapControlPolyline;

View File

@@ -0,0 +1,30 @@
import { useEffect } from "react";
import { Polygon, Polyline, useMap } from "react-leaflet";
const OldBoundLayer = ({ bound }) => {
const map = useMap();
const safeFitBounds = (coordinates) => {
if (!map) return;
try {
const latLngs = coordinates;
map.fitBounds(latLngs, {
paddingTopLeft: [70, 70],
paddingBottomRight: [70, 70],
});
} catch (e) {
console.warn("fitBounds failed:", e);
}
};
useEffect(() => {
safeFitBounds(bound.coordinates);
}, []);
return bound.type == "polygon" ? (
<Polygon pathOptions={{ color: "purple", stroke: false }} positions={bound.coordinates} />
) : (
<Polyline pathOptions={{ color: "purple" }} positions={bound.coordinates} />
);
};
export default OldBoundLayer;

View File

@@ -0,0 +1,25 @@
import SelectBox from "@/core/components/SelectBox";
export const boundTypes = [
{ id: "polygon", name_fa: "محدوده" },
{ id: "polyline", name_fa: "مسیر" },
];
const SelectBoundType = ({ boundType, setBoundType, setBound, setAllData }) => {
return (
<>
<SelectBox
value={boundType}
label="نوع منطقه عملیاتی"
selectors={boundTypes}
schema={{ name: "name_fa", value: "id" }}
onChange={(newValue) => {
setBound(null);
setBoundType(newValue);
setAllData({ bound_type: newValue });
}}
/>
</>
);
};
export default SelectBoundType;

View File

@@ -0,0 +1,56 @@
import MapLoading from "@/core/components/MapLayer/Loading";
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
import dynamic from "next/dynamic";
import { useCallback, useState } from "react";
import MapControlPolygon from "./MapControlPolygon";
import MapControlPolyline from "./MapControlPolyline";
import SelectBoundType from "./SelectBoundType";
import OldBoundLayer from "./OldBoundLayer";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
const [bound, setBound] = useState(allData.bound);
const [boundType, setBoundType] = useState(allData.bound_type);
const handleNext = useCallback(() => {
setAllData({ bound: bound });
setTabState((s) => s + 1);
}, [bound]);
return (
<>
<DialogContent dividers>
<Stack spacing={2}>
<SelectBoundType
boundType={boundType}
setBoundType={setBoundType}
setBound={setBound}
setAllData={setAllData}
/>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
{boundType == "polygon" ? (
<MapControlPolygon bound={bound} setBound={setBound} boundType={boundType} />
) : (
<MapControlPolyline bound={bound} setBound={setBound} boundType={boundType} />
)}
<OldBoundLayer bound={oldBound} />
</MapLayer>
</Box>
</Stack>
</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,60 @@
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" });
const now = new Date();
return (
type && (
<>
<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={now}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
/>
</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}
views={["hours"]}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
/>
</Grid>
)}
</>
)
);
};
export default MissionDates;

View File

@@ -0,0 +1,83 @@
import SelectBox from "@/core/components/SelectBox";
import StyledForm from "@/core/components/StyledForm";
import { missionTypes } from "@/core/utils/missionTypes";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
import moment from "jalali-moment";
import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup";
import MissionDates from "./MissionDates";
const validationSchema = object({
type: string().required("نوع ماموریت را مشخص کنید!"),
end_date: string().required("تاریخ پایان ماموریت را مشخص کنید!"),
end_time: string().when("type", {
is: "1",
then: (schema) => schema.required("زمان پایان ماموریت را مشخص کنید!"),
otherwise: (schema) => schema.notRequired(),
}),
});
const GetDateTime = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = {
type: allData.type,
end_date: allData.end_date,
end_time: allData.end_time ? moment(allData.end_time).toDate() : null,
};
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}>
<Controller
control={control}
name={"type"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="نوع زمانبندی"
selectors={missionTypes}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
/>
)}
/>
</Grid>
<Grid item xs={12}>
<Alert severity="info">
تاریخ شروع مأموریت پس از ثبت مأموریت بهصورت خودکار تعیین میشود.
</Alert>
</Grid>
<MissionDates control={control} />
</Grid>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" type="submit">
مرحله بعد
</Button>
</DialogActions>
</StyledForm>
);
};
export default GetDateTime;

View File

@@ -0,0 +1,20 @@
import { Done } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material";
const SelectId = ({ row, onChange, setOpenFastReactDialog }) => {
const handleClick = () => {
onChange(row.original.id);
setOpenFastReactDialog(false);
};
return (
<>
<Tooltip title="انتخاب" arrow placement="right">
<IconButton color="primary" sx={{ textTransform: "unset", alignSelf: "center" }} onClick={handleClick}>
<Done />
</IconButton>
</Tooltip>
</>
);
};
export default SelectId;

View File

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

View File

@@ -0,0 +1,274 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_FAST_REACT_COMPLAINTS } from "@/core/utils/routes";
import { Box, Stack, Typography } from "@mui/material";
import { useMemo } from "react";
import RowActions from "./RowActions";
import { usePermissions } from "@/lib/hooks/usePermissions";
import { useAuth } from "@/lib/contexts/auth";
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import DescriptionForm from "@/components/dashboard/fastReact/complaintList/RowActions/DescriptionDialog";
import LocationForm from "@/components/dashboard/fastReact/complaintList/RowActions/LocationDialog";
import moment from "jalali-moment";
import useProvinces from "@/lib/hooks/useProvince";
import useEdaratLists from "@/lib/hooks/useEdaratLists";
const FastReactList = ({ onChange, setOpenFastReactDialog }) => {
const { data: userPermissions } = usePermissions();
const hasCountryPermission = userPermissions?.includes("show-fast-react");
const { user } = useAuth();
const columns = useMemo(() => {
const dynamicColumns = {
header: "استان",
id: "road_observeds__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: ({ row }) => <>{row.original.province_fa}</>,
};
return [
{
accessorKey: "id",
header: "کد یکتا",
id: "road_observeds__id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
...(hasCountryPermission ? [dynamicColumns] : []),
{
header: "اداره",
id: "road_observeds__edarate_shahri_id",
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
dependencyId: hasCountryPermission ? "road_observeds__province_id" : null,
grow: false,
size: 120,
ColumnSelectComponent: (props) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
);
const [prevDependency, setPrevDependency] = useState(
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
);
const getColumnSelectOptions = useMemo(() => {
if (hasCountryPermission && props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingEdaratList) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorEdaratList) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
{ value: "", label: "کل ادارات" },
...edaratList.map((edare) => ({
value: edare.id,
label: edare.name_fa,
})),
];
}, [edaratList, loadingEdaratList, errorEdaratList]);
useEffect(() => {
if (hasCountryPermission) return;
if (prevDependency === props.dependencyFieldValue?.value) return;
props.handleChange({ ...props.filterParameters, value: "" });
setPrevDependency(props.dependencyFieldValue?.value);
}, [props.dependencyFieldValue?.value, hasCountryPermission]);
return (
<CustomSelectByDependency
{...props}
value={
props.dependencyFieldValue?.value === ""
? "empty"
: loadingEdaratList
? "loading"
: props.filterParameters.value
}
columnSelectOption={getColumnSelectOptions}
/>
);
},
Cell: ({ row }) => <>{row.original.edarate_shahri_name_fa}</>,
},
{
header: "اطلاعات ثبت‌ شده در سامانه سوانح",
id: "fkInfo",
enableColumnFilter: false,
enableSorting: false,
grow: false,
size: 50,
columns: [
{
accessorKey: "fk_RegisteredEventMessage",
header: "کد سوانح",
id: "fk_RegisteredEventMessage",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "Title",
header: "موضوع گزارش",
id: "Title",
enableColumnFilter: false,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "FeatureTypeTitle",
header: "نوع گزارش",
id: "FeatureTypeTitle",
enableColumnFilter: false,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "Description",
header: "توضیح گزارش",
id: "Description",
enableColumnFilter: false,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
py: 0,
"&:first-of-type": {
borderLeft: "unset",
},
},
},
Cell: ({ renderedCellValue }) => {
if (renderedCellValue) {
return (
<Stack alignItems={"center"} justifyContent={"center"}>
<DescriptionForm description={renderedCellValue} title={"توضیح گزارش"} />
</Stack>
);
}
return <Typography variant="body2">-</Typography>;
},
},
{
header: "موقعیت",
id: "location",
enableColumnFilter: false,
enableSorting: false,
datatype: "array",
grow: false,
size: 100,
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
py: 0,
"&:first-of-type": {
borderLeft: "unset",
},
},
},
Cell: ({ row }) => {
return (
<Stack alignItems={"center"} justifyContent={"center"}>
<LocationForm start_lat={row.original.lat} start_lng={row.original.lng} />
</Stack>
);
},
},
{
accessorKey: "MobileForSendEventSms",
header: "شماره تماس گیرنده",
id: "MobileForSendEventSms",
enableColumnFilter: false,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorFn: (row) => moment(row.StartTime_DateTime).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ ثبت",
id: "StartTime_DateTime",
enableColumnFilter: true,
datatype: "date",
filterMode: "between",
grow: false,
size: 100,
},
],
},
];
}, []);
return (
<>
<Box sx={{ mt: 3, p: 1 }}>
<DataTableWithAuth
need_filter={true}
table_title="لیست شکایات"
columns={columns}
sorting={[{ id: "StartTime_DateTime", desc: true }]}
table_url={GET_FAST_REACT_COMPLAINTS}
page_name={"roadMissionsOperator"}
table_name={"roadMissionsOperatorFastReactList"}
enableRowActions
positionActionsColumn={"first"}
RowActions={(props) => (
<RowActions {...props} onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
)}
/>
</Box>
</>
);
};
export default FastReactList;

View File

@@ -0,0 +1,49 @@
import { Close, Edit } from "@mui/icons-material";
import { Button, Chip, Dialog, Divider, IconButton } from "@mui/material";
import FastReactList from "./List";
import { useState, forwardRef } from "react";
const FastReactDialog = forwardRef(({ onChange, value }, ref) => {
const [openFastReactDialog, setOpenFastReactDialog] = useState(false);
return (
<>
<Divider ref={ref}>
{value === "" ? (
<Button onClick={() => setOpenFastReactDialog(true)} variant="outlined">
انتخاب شکایت
</Button>
) : (
<Chip
label={`کد شکایت: ${value}`}
variant="outlined"
deleteIcon={<Edit />}
onDelete={() => setOpenFastReactDialog(true)}
/>
)}
</Divider>
<Dialog open={openFastReactDialog} fullWidth maxWidth="md">
<IconButton
aria-label="close"
onClick={() => setOpenFastReactDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
{openFastReactDialog && (
<FastReactList onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
)}
</Dialog>
</>
);
});
FastReactDialog.displayName = "FastReactDialog";
export default FastReactDialog;

View File

@@ -0,0 +1,22 @@
import { Divider, Stack } from "@mui/material";
import { Controller, useWatch } from "react-hook-form";
import FastReactDialog from "./Dialog";
const FastReactCode = ({ control }) => {
const category_id = useWatch({ control, name: "category_id" });
if (category_id != 3) return null;
return (
<Controller
control={control}
name={"road_observed_id"}
render={({ field }) => (
<Stack justifyContent={"center"} sx={{ height: "100%" }}>
<FastReactDialog {...field} />
</Stack>
)}
/>
);
};
export default FastReactCode;

View File

@@ -0,0 +1,153 @@
import PersianTextField from "@/core/components/PersianTextField";
import SelectBox from "@/core/components/SelectBox";
import StyledForm from "@/core/components/StyledForm";
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
import { missionRegions } from "@/core/utils/missionRegions";
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 FastReactCode from "./FastReactCode";
const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"),
end_point: string().required("مقصد را مشخص کنید!"),
region: string().required("محور ماموریت را مشخص کنید!"),
category_id: string().required("نوع ماموریت را مشخص کنید!"),
road_observed_id: string().when("category_id", {
is: "3",
then: (schema) => schema.required("کد یکتا شکایت را مشخص کنید!"),
otherwise: (schema) => schema.notRequired(),
}),
});
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
const defaultValues = {
explanation: allData.explanation,
end_point: allData.end_point,
region: allData.region,
category_id: allData.category_id,
road_observed_id: allData.road_observed_id,
};
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={"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 }}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"region"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="محدوده"
selectors={missionRegions}
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={"category_id"}
render={({ field, fieldState: { error } }) => (
<SelectBox
value={field.value}
label="نوع ماموریت"
selectors={missionCategoryTypes}
schema={{ name: "name_fa", value: "id" }}
error={error}
onChange={field.onChange}
helperText={error?.message}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FastReactCode control={control} />
</Grid>
<Grid item xs={12} sm={6}>
<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,134 @@
import MapLoading from "@/core/components/MapLayer/Loading";
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
import { missionRegions } from "@/core/utils/missionRegions";
import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typography } from "@mui/material";
import moment from "jalali-moment";
import dynamic from "next/dynamic";
import { useCallback } from "react";
import ShowBound from "../../../../../../Actions/showBound";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Verify = ({ 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={allData.explanation} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">محدوده</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={missionRegions.find((r) => r.id == allData.region).name_fa} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">نوع ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
<Chip
size="small"
label={missionCategoryTypes.find((t) => t.id == allData.category_id).name_fa}
/>
</Stack>
{allData.category_id == 3 && (
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">کد شکایت</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={allData.road_observed_id} />
</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 direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={"از لحظه ثبت ماموریت"} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ پایان ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
{allData.type == 1 ? (
<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>
<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={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
/>
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">راننده</Typography>
<Divider sx={{ flex: 1 }} />
<Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} />
</Stack>
</Stack>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="همراهان" />
</Divider>
{allData.rahdaran.length != 0 ? (
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 direction={"row"} alignItems={"center"} justifyContent={"center"}>
<Typography variant="body2">همراهی ثبت نشده است</Typography>
</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,102 @@
import { AccessTime, InsertDriveFile, Route, Verified } from "@mui/icons-material";
import { Box, Tab, Tabs } from "@mui/material";
import { useReducer, useState } from "react";
import Area from "./Area";
import GetDateTime from "./GetDateTime";
import GetItemInfo from "./GetItemInfo";
import Verify from "./Verify";
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, oldBound }) => {
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={<AccessTime />} label="زمانبندی" />
<Tab disabled={tabState < 2} icon={<Route />} label="منطقه عملیاتی" />
<Tab disabled={tabState < 3} icon={<Verified />} label="بررسی نهایی" />
</Tabs>
<TabPanel value={tabState} index={0}>
<GetItemInfo
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={1}>
<GetDateTime
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={2}>
<Area
oldBound={oldBound}
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });
}}
handlePrev={handlePrev}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={3}>
<Verify allData={allData} handlePrev={handlePrev} submitForm={submitForm} submitting={submitting} />
</TabPanel>
</>
);
};
export default CreateForm;

View File

@@ -0,0 +1,83 @@
import { REQUEST_MISSION_CONTINUE_MISSION } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import moment from "jalali-moment";
import { useState } from "react";
import CreateForm from "./Form";
const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true });
const submitForm = async (result) => {
setSubmitting(true);
const bound = result.bound.getLatLngs();
let area =
result.bound_type == "polygon"
? bound.map((ring) => ring.map((latlng) => [latlng.lat, latlng.lng]))[0]
: bound.map((latlng) => [latlng.lat, latlng.lng]);
await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", {
data: {
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
machines: [result.machine.id],
driver: result.driver.id,
zone: result.region,
type: result.type,
...(result.type == 1
? {
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
}
: {
end_date: result.end_date,
}),
end_point: result.end_point,
area: {
type: result.bound_type,
coordinates: area,
},
category_id: result.category_id,
explanation: result.explanation,
...(result.category_id == 3
? {
road_observed_id: result.road_observed_id,
}
: {}),
},
hasSidebarUpdate: true,
})
.then((response) => {
mutate();
setOpen(false);
})
.catch((error) => {})
.finally(() => {
setSubmitting(false);
});
};
return (
<CreateForm
defaultValues={{
explanation: "",
category_id: "",
bound: null,
rahdaran: rahdaran.filter((r) => !r.is_driver),
bound_type: "polygon",
type: "",
end_date: "",
end_time: null,
end_point: "",
region: "",
machine: machine,
driver: rahdaran.find((r) => r.is_driver),
}}
oldBound={oldBound}
submitForm={submitForm}
submitting={submitting}
open={open}
setOpen={setOpen}
mutate={mutate}
/>
);
};
export default DialogAdd;

View File

@@ -0,0 +1,52 @@
import DialogLoading from "@/core/components/DialogLoading";
import { GET_MACHINES_BY_ID, GET_RAHDARAN_BY_ID } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import L from "leaflet";
import { useEffect, useMemo, useState } from "react";
import DialogAdd from "./Dialog";
const AddController = ({ row, mutate, setOpenAddDialog }) => {
const [rahdaran, setRahdaran] = useState(null);
const [rahdaranLoading, setRahdaranLoading] = useState(true);
const [machine, setMachine] = useState(null);
const [machineLoading, setMachineLoading] = useState(true);
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]);
useEffect(() => {
setMachineLoading(true);
requestServer(`${GET_MACHINES_BY_ID}/${row.original.id}`, "get")
.then((response) => {
setMachine(response.data.data[0]);
setMachineLoading(false);
})
.catch((e) => {
setMachineLoading(false);
});
}, [row.original.id]);
return rahdaranLoading || machineLoading ? (
<DialogLoading />
) : (
<DialogAdd
mutate={mutate}
setOpen={setOpenAddDialog}
oldBound={row.original.area}
rahdaran={rahdaran}
machine={machine}
row={row}
/>
);
};
export default AddController;

View File

@@ -0,0 +1,41 @@
import { Add, Close } from "@mui/icons-material";
import { Dialog, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import AddController from "./AddController";
const AddDialog = ({ row, mutate }) => {
const [openAddDialog, setOpenAddDialog] = useState(false);
return (
<>
<Tooltip title="اتصال ماموریت جدید" arrow placement="right">
<IconButton
color="primary"
sx={{ textTransform: "unset", alignSelf: "center" }}
onClick={() => {
setOpenAddDialog(true);
}}
>
<Add />
</IconButton>
</Tooltip>
<Dialog open={openAddDialog} fullWidth maxWidth="sm">
<IconButton
aria-label="close"
onClick={() => setOpenAddDialog(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
zIndex: 50,
color: theme.palette.grey[500],
})}
>
<Close />
</IconButton>
{openAddDialog && <AddController setOpenAddDialog={setOpenAddDialog} row={row} mutate={mutate} />}
</Dialog>
</>
);
};
export default AddDialog;

View File

@@ -16,7 +16,7 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
);
const [submitting, setSubmitting] = useState(false);
const [rahdaran, setRahdaran] = useState(null);
const [rahdaranLoading, setRahdaranLoading] = useState(false);
const [rahdaranLoading, setRahdaranLoading] = useState(true);
const requestServer = useRequest();
useEffect(() => {
@@ -87,7 +87,7 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
explanation: row.original.explanation,
category_id: row.original.category_id,
road_observed_id: row.original.category_id == 3 ? row.original.road_observed[0].id : "",
rahdaran: rahdaran,
rahdaran: rahdaran.filter((r) => !r.isDriver),
requested_machines: row.original.requested_machines,
bound: bound,
bound_type: row.original.area.type,

View File

@@ -2,6 +2,7 @@ import { Box } from "@mui/material";
import Edit from "./Edit";
import Delete from "./Delete";
import { useAuth } from "@/lib/contexts/auth";
import Add from "./Add";
const RowActions = ({ row, mutate }) => {
const { user } = useAuth();
@@ -14,6 +15,11 @@ const RowActions = ({ row, mutate }) => {
<Delete mutate={mutate} rowId={row.original.id} />
</>
)}
{row.original.state_id == 3 && user.id == row.original.user_id && (
<>
<Add mutate={mutate} row={row} />
</>
)}
</Box>
);
};

View File

@@ -12,6 +12,7 @@ import { missionTypes } from "@/core/utils/missionTypes";
import moment from "jalali-moment";
import MachinesDialog from "./MachinesDialog";
import DriversDialog from "./DriversDialog";
import LtrTextField from "@/core/components/LtrTextField";
const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
const bound = useMemo(
() =>
@@ -21,14 +22,16 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
const requestServer = useRequest({ notificationSuccess: true });
const [machine, setMachine] = useState();
const [driver, setDriver] = useState();
const [kilometer, setKilometer] = useState();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (_machine, _driver) => {
const handleSubmit = async (_machine, _driver, _kilometer) => {
setIsSubmitting(true);
await requestServer(`${ALLOCATE_REQUEST_MISSION}/${row.id}`, "post", {
data: {
machines: [_machine.id],
driver: _driver.id,
km: kilometer,
},
hasSidebarUpdate: true,
})
@@ -107,6 +110,23 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
)}
</Stack>
)}
{driver && (
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<LtrTextField
autoComplete="off"
type="tel"
value={kilometer}
onChange={(e) => {
if (isNaN(Number(e.target.value))) return;
setKilometer(e.target.value);
}}
size="small"
fullWidth
label="کیلومتر خودرو"
InputLabelProps={{ shrink: true }}
/>
</Stack>
)}
</Stack>
</Paper>
<Stack spacing={1}>
@@ -188,10 +208,10 @@ const AllocationForm = ({ row, mutate, setOpenAllocationDialog }) => {
</Stack>
<Box>
<Button
onClick={() => handleSubmit(machine, driver)}
onClick={() => handleSubmit(machine, driver, kilometer)}
variant="contained"
color="primary"
disabled={!machine || !driver || isSubmitting}
disabled={!machine || !driver || !kilometer || isSubmitting}
autoFocus
>
تایید و تخصیص

View File

@@ -214,3 +214,31 @@ export const errorClientToast = (toastContainer) =>
draggable: false,
}
);
export const errorMaxLengthToast = (toastContainer) =>
toast.error(
() => (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
justifyContent: "start",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Dangerous color="error" sx={{ mr: 1.6 }} />
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography variant="caption">{"حجم درخواست بیشتر از حد مجاز است."}</Typography>
</Box>
</Box>
</Box>
),
{
icon: false,
containerId: toastContainer,
autoClose: false,
closeOnClick: false,
draggable: false,
}
);

View File

@@ -4,6 +4,7 @@ import {
errorAccessDeniedToast,
errorClientToast,
errorLogicToast,
errorMaxLengthToast,
errorServerToast,
errorTooManyToast,
errorUnauthorizedToast,
@@ -26,6 +27,9 @@ const errorClient = (response, notification, toastContainer, logout) => {
case 403:
if (notification) errorAccessDeniedToast(response.data.message, toastContainer);
break;
case 413:
if (notification) errorMaxLengthToast(response.data.message, toastContainer);
break;
case 422:
if ("type" in response.data) {
if (Array.isArray(response.data.message)) {

View File

@@ -150,7 +150,7 @@ export const pageMenu = [
id: "roadItemManagmentSupervisorCartable",
label: "کارتابل",
type: "page",
route: "/dashboard/road-items/supervisor",
route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/supervisor/cartable",
permissions: [
"show-road-item-supervise-cartable",
"show-road-item-supervise-cartable-province",
@@ -170,7 +170,7 @@ export const pageMenu = [
id: "roadItemManagmentOparationCartable",
label: "کارتابل",
type: "page",
route: "/dashboard/road-items/operator",
route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/operator/cartable",
permissions: ["create-road-item"],
},
],
@@ -179,7 +179,7 @@ export const pageMenu = [
id: "roadItemManagmentMap",
label: "پراکندگی بر روی نقشه",
type: "page",
route: "/dashboard/road-items/reports/map",
route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_items",
icon: <MapIcon sx={{ width: "inherit", height: "inherit" }} />,
permissions: [
"show-road-item-supervise-cartable",
@@ -187,37 +187,37 @@ export const pageMenu = [
"create-road-item",
],
},
{
id: "roadItemManagmentReport",
label: "گزارش ها",
type: "menu",
icon: <AssessmentIcon sx={{ width: "inherit", height: "inherit" }} />,
hasSubitems: true,
Subitems: [
{
id: "roadItemManagmentReportProvince",
label: "تعداد فعالیت ها به تفکیک آیتم",
type: "page",
route: "/dashboard/road-items/reports/items-report",
permissions: [
"show-road-item-supervise-cartable",
"show-road-item-supervise-cartable-province",
"create-road-item",
],
},
{
id: "roadItemManagmentReportItems",
label: "تعداد و حجم فعالیت ها به تفکیک موارد اقدام شده",
type: "page",
route: "/dashboard/road-items/reports/sub-items-report",
permissions: [
"show-road-item-supervise-cartable",
"show-road-item-supervise-cartable-province",
"create-road-item",
],
},
],
},
// {
// id: "roadItemManagmentReport",
// label: "گزارش ها",
// type: "menu",
// icon: <AssessmentIcon sx={{ width: "inherit", height: "inherit" }} />,
// hasSubitems: true,
// Subitems: [
// {
// id: "roadItemManagmentReportProvince",
// label: "تعداد فعالیت ها به تفکیک آیتم",
// type: "page",
// route: "/dashboard/road-items/reports/items-report",
// permissions: [
// "show-road-item-supervise-cartable",
// "show-road-item-supervise-cartable-province",
// "create-road-item",
// ],
// },
// {
// id: "roadItemManagmentReportItems",
// label: "تعداد و حجم فعالیت ها به تفکیک موارد اقدام شده",
// type: "page",
// route: "/dashboard/road-items/reports/sub-items-report",
// permissions: [
// "show-road-item-supervise-cartable",
// "show-road-item-supervise-cartable-province",
// "create-road-item",
// ],
// },
// ],
// },
],
},
{
@@ -581,7 +581,7 @@ export const pageMenu = [
type: "page",
route: "/dashboard/rahdaran",
icon: <HandymanIcon sx={{ width: "inherit", height: "inherit" }} />,
permissions: ["show-rahdaran"],
permissions: ["manage-rahdaran-country", "manage-rahdaran-province", "manage-rahdaran-edareh"],
},
{
id: "adminManagement",

View File

@@ -580,7 +580,7 @@ export const pageMenuDev = [
type: "page",
route: "/dashboard/rahdaran",
icon: <HandymanIcon sx={{ width: "inherit", height: "inherit" }} />,
permissions: ["show-rahdaran"],
permissions: ["manage-rahdaran-country", "manage-rahdaran-province", "manage-rahdaran-edareh"],
},
{
id: "adminManagement",

View File

@@ -217,6 +217,7 @@ export const GET_ROAD_MISSIONS_TRANSPORTATION_LIST = api + "/api/v3/missions/tra
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 REQUEST_MISSION_WITHOUT_PROCESS = api + "/api/v3/missions/request_portal/no_process";
export const REQUEST_MISSION_CONTINUE_MISSION = api + "/api/v3/missions/request_portal/continue";
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";