Merge branch 'feature/gasht_submit' into 'develop'

Feature/gasht submit

See merge request witel-front-end/rms!55
This commit is contained in:
AmirHossein Mahmoodi
2025-01-06 09:42:15 +00:00
29 changed files with 1698 additions and 77 deletions

View File

@@ -3,7 +3,6 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { array, mixed, number, object, string } from "yup";
import { useTheme } from "@emotion/react";
import useRequest from "@/lib/hooks/useRequest";
import { Box, Button, DialogActions, DialogContent, Tab, Tabs, useMediaQuery } from "@mui/material";
import StyledForm from "@/core/components/StyledForm";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
@@ -15,7 +14,6 @@ import BeenhereIcon from "@mui/icons-material/Beenhere";
import GetItemsForm from "@/components/dashboard/roadItems/operator/Actions/Create/Forms/GetItemsForm";
import GetSubItemsForm from "@/components/dashboard/roadItems/operator/Actions/Create/Forms/GetSubItemsForm";
import GetItemInfo from "@/components/dashboard/roadItems/operator/Actions/Create/Forms/GetItemInfo";
import { CREATE_ROAD_ITEMS } from "@/core/utils/routes";
function TabPanel(props) {
const { children, value, index } = props;
@@ -26,27 +24,30 @@ function TabPanel(props) {
);
}
const defaultValues = {
item_id: null,
sub_item_id: null,
amount: "",
activity_time: "",
activity_date: "",
before_image: null,
after_image: null,
cmms_machines: null,
rahdaran_id: null,
start_point: "",
end_point: "",
};
const validationSchema = object({
item_id: number().required("نوع آیتم را مشخص کنید!"),
sub_item_id: number().required("موضوع مشاهده شده را مشخص کنید!"),
amount: string().required("وارد کردن مقدار الزامیست!"),
cmms_machines: array().required("وارد کردن کد خودرو الزامیست!"),
rahdaran_id: array().required("وارد کردن کد راهداران الزامیست!").min(1, "حداقل یک کد راهدار باید وارد شود!"), // بررسی حداقل یک آیتم,
activity_time: string().required("لطفا زمان فعالیت را انتخاب کنید!"),
activity_date: string().required("لطفاً تاریخ شروع فعالیت را انتخاب کنید!"),
activity_time: string().test("activity-time-conditional", "لطفا زمان فعالیت را انتخاب کنید!", function (value) {
const { is_gasht } = this.options.context; // دسترسی به context
if (is_gasht) {
return true; // اگر is_gasht true باشد، این فیلد نیاز به اعتبارسنجی ندارد
}
return !!value; // در غیر این صورت مقدار فیلد باید وجود داشته باشد
}),
activity_date: string().test(
"activity-date-conditional",
"لطفاً تاریخ شروع فعالیت را انتخاب کنید!",
function (value) {
const { is_gasht } = this.options.context; // دسترسی به context
if (is_gasht) {
return true; // اگر is_gasht true باشد، این فیلد نیاز به اعتبارسنجی ندارد
}
return !!value; // در غیر این صورت مقدار فیلد باید وجود داشته باشد
}
),
before_image: mixed()
.nullable()
.test("before-image-required", "لطفا عکس قبل از اقدام را بارگذاری کنید!", function (value) {
@@ -82,7 +83,20 @@ const validationSchema = object({
}),
});
const CreateFormContent = ({ setOpen, onSubmit }) => {
const CreateFormContent = ({ setOpen, onSubmit, is_gasht = false }) => {
const defaultValues = {
item_id: null,
sub_item_id: null,
amount: "",
before_image: null,
after_image: null,
cmms_machines: null,
rahdaran_id: null,
start_point: "",
end_point: "",
...(!is_gasht && { activity_time: "", activity_date: "" }),
};
const [tabState, setTabState] = useState(0);
const [itemsList, setItemsList] = useState();
const [subItemsList, setSubItemsList] = useState([]);
@@ -131,7 +145,7 @@ const CreateFormContent = ({ setOpen, onSubmit }) => {
defaultValues,
resolver: yupResolver(validationSchema),
mode: "onBlur",
context: { subItemsList },
context: { subItemsList, is_gasht },
});
const onSubmitBase = async (data) => {
let result = { ...data };
@@ -180,9 +194,10 @@ const CreateFormContent = ({ setOpen, onSubmit }) => {
},
});
};
console.log(errors);
return (
<StyledForm onSubmit={handleSubmit(onSubmitBase)}>
<StyledForm onSubmit={handleSubmit(onSubmitBase)} id="road_items">
<Tabs
allowScrollButtonsMobile
value={tabState}
@@ -230,6 +245,7 @@ const CreateFormContent = ({ setOpen, onSubmit }) => {
register={register}
watch={watch}
getValues={getValues}
is_gasht={is_gasht}
/>
</TabPanel>
</Box>
@@ -250,6 +266,7 @@ const CreateFormContent = ({ setOpen, onSubmit }) => {
size="large"
disabled={isSubmitting}
type={"submit"}
form="road_items"
endIcon={<BeenhereIcon />}
>
{isSubmitting ? "در حال ثبت فعالیت" : "ثبت فعالیت"}

View File

@@ -1,4 +1,4 @@
import { Stack, TextField, Grid } from "@mui/material";
import { Grid, Stack } from "@mui/material";
import ImageUpload from "./ImageUpload";
import MuiDatePicker from "@/core/components/MuiDatePicker";
import MuiTimePicker from "@/core/components/MuiTimePicker";
@@ -9,7 +9,8 @@ import CarCode from "@/core/components/CarCode";
import RahdarCode from "@/core/components/RahdarCode";
import PreviousStatesInfo from "./PreviousStatesInfo";
import NumberField from "@/core/components/NumberField";
const GetItemInfo = ({ register, itemsList, subItemsList, control, setValue, errors, watch, getValues }) => {
const GetItemInfo = ({ register, itemsList, subItemsList, control, setValue, errors, watch, getValues, is_gasht }) => {
return (
<Stack spacing={2}>
<Stack>
@@ -77,30 +78,32 @@ const GetItemInfo = ({ register, itemsList, subItemsList, control, setValue, err
<ImageUpload setValue={setValue} control={control} errors={errors} getValues={getValues} />
) : null}
</Stack>
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<MuiDatePicker
name="activity_date"
error={errors.activity_date ? errors.activity_date.message : null}
value={watch("activity_date")}
placeholder={"تاریخ شروع فعالیت"}
setFieldValue={setValue}
helperText={errors.activity_date ? errors.activity_date.message : null}
/>
{!is_gasht ? (
<Stack>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<MuiDatePicker
name="activity_date"
error={errors.activity_date ? errors.activity_date.message : null}
value={watch("activity_date")}
placeholder={"تاریخ شروع فعالیت"}
setFieldValue={setValue}
helperText={errors.activity_date ? errors.activity_date.message : null}
/>
</Grid>
<Grid item xs={12} md={6}>
<MuiTimePicker
value={watch("activity_time")}
name="activity_time"
error={errors.activity_time ? errors.activity_time.message : null}
placeholder={"زمان فعالیت"}
setFieldValue={setValue}
helperText={errors.activity_time ? errors.activity_time.message : null}
/>
</Grid>
</Grid>
<Grid item xs={12} md={6}>
<MuiTimePicker
value={watch("activity_time")}
name="activity_time"
error={errors.activity_time ? errors.activity_time.message : null}
placeholder={"زمان فعالیت"}
setFieldValue={setValue}
helperText={errors.activity_time ? errors.activity_time.message : null}
/>
</Grid>
</Grid>
</Stack>
</Stack>
) : null}
<Stack>
{subItemsList?.needs_end_point === 1 ? (
<MapInfoTwoMarker setValue={setValue} errors={errors} />

View File

@@ -15,15 +15,31 @@ const ImageUpload = ({ control, setValue, errors, getValues, beforeImage = null,
const [showAfterImage, setShowAfterImage] = useState(!afterImage);
useEffect(() => {
if (getValues("before_image")) {
const beforeImage = getValues("before_image");
const afterImage = getValues("after_image");
if (beforeImage) {
setShowBeforeImage(false);
setBeforeImg(getValues("before_image"));
setBeforeFileType("image/");
if (typeof beforeImage === "string") {
setBeforeImg(beforeImage);
setBeforeFileType("image/");
} else if (beforeImage instanceof File) {
setBeforeImg(URL.createObjectURL(beforeImage));
setBeforeFileType(beforeImage.type);
}
}
if (getValues("after_image")) {
if (afterImage) {
setShowAfterImage(false);
setAfterImg(getValues("after_image"));
setAfterFileType("image/");
if (typeof afterImage === "string") {
setAfterImg(afterImage);
setAfterFileType("image/");
} else if (afterImage instanceof File) {
setAfterImg(URL.createObjectURL(afterImage));
setAfterFileType(afterImage.type);
}
}
}, [getValues]);

View File

@@ -14,21 +14,34 @@ const EditFormContent = ({ setOpenEditDialog, onSubmit, defaultData }) => {
start_point: defaultData?.start_point || { lat: "", lng: "" },
end_point: defaultData?.end_point || { lat: "", lng: "" },
cmms_machines: defaultData?.cmms_machines || null,
rahdaran_id: defaultData?.rahdaran || null,
rahdaran_id: defaultData?.rahdaran_id || null,
};
const onSubmitBase = async (data) => {
let result = { ...data };
(result.before_image === null || result.before_image === defaultValues.before_image) &&
if (result.before_image === null) {
delete result.before_image;
(result.after_image === null || result.after_image === defaultValues.after_image) && delete result.after_image;
if (subItem.needs_end_point !== 1) {
delete data.before_image;
}
if (result.after_image === null) {
delete result.after_image;
delete data.after_image;
}
if (result.end_point === "") {
delete result.end_point;
delete data.end_point;
} else {
result.end_point = `${result.end_point.lat},${result.end_point.lng}`;
}
result.start_point = `${result.start_point.lat},${result.start_point.lng}`;
if (result.start_point === "") {
delete result.start_point;
delete data.start_point;
} else {
result.start_point = `${result.start_point.lat},${result.start_point.lng}`;
}
result.rahdaran_id.forEach((rahdar, index) => {
result[`rahdaran_id[${index}]`] = rahdar.id;
@@ -45,8 +58,10 @@ const EditFormContent = ({ setOpenEditDialog, onSubmit, defaultData }) => {
data: {
...data,
item_name: subItem.item_str,
sub_item_name: subItemsList.name,
unit_fa: subItemsList.unit,
sub_item_name: subItem.name,
unit_fa: subItem.unit,
item_id: subItem.item,
sub_item_id: subItem.sub_item,
},
});
};

View File

@@ -65,15 +65,16 @@ const EditFormCreate = ({ defaultData, subItem, onSubmitBase, defaultValues, set
mode: "onBlur",
context: { subItem },
});
return (
<StyledForm onSubmit={handleSubmit(onSubmitBase)}>
<StyledForm onSubmit={handleSubmit(onSubmitBase)} id="edit_road_items">
<DialogContent sx={{ overflowY: "auto", maxHeight: "70vh" }}>
<Stack spacing={2}>
<Stack>
{subItem.needs_image === 1 ? (
<ImageUpload
afterImage={defaultData?.afterImage}
beforeImage={defaultData?.beforeImage}
afterImage={defaultData?.after_image}
beforeImage={defaultData?.before_image}
getValues={getValues}
setValue={setValue}
control={control}
@@ -161,7 +162,13 @@ const EditFormCreate = ({ defaultData, subItem, onSubmitBase, defaultValues, set
<Button onClick={() => setOpenEditDialog(false)} variant="outlined" color="secondary" autoFocus>
بستن
</Button>
<Button disabled={isSubmitting} type={"submit"} variant="contained" color="primary">
<Button
form="edit_road_items"
disabled={isSubmitting}
type={"submit"}
variant="contained"
color="primary"
>
{isSubmitting ? "در حال ویرایش" : "ویرایش"}
</Button>
</DialogActions>

View File

@@ -0,0 +1,33 @@
"use client";
import { Button, IconButton, useMediaQuery } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { AddCircle } from "@mui/icons-material";
import CreatePatrol from "../../Forms/CreatePatrol";
const OperatorCreate = ({ mutate }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
return (
<>
{isMobile ? (
<IconButton aria-label="ثبت اقدام جدید" color="primary" onClick={handleOpen}>
<AddCircle sx={{ fontSize: "25px" }} />
</IconButton>
) : (
<Button variant="contained" color="primary" startIcon={<AddCircle />} onClick={handleOpen}>
ثبت اقدام
</Button>
)}
{open && <CreatePatrol open={open} setOpen={setOpen} mutate={mutate} />}
</>
);
};
export default OperatorCreate;

View File

@@ -45,7 +45,6 @@ const PrintExcel = ({ table, filterData }) => {
<Button
color="primary"
variant="contained"
size="small"
disabled={loading}
sx={{ textTransform: "unset", alignSelf: "center" }}
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}

View File

@@ -0,0 +1,85 @@
"use client";
import React from "react";
import dynamic from "next/dynamic";
import MapLoading from "@/core/components/MapLayer/Loading";
import {
Box,
Card,
CardActions,
CardContent,
CardHeader,
Chip,
Divider,
IconButton,
Stack,
Typography,
} from "@mui/material";
import HandymanIcon from "@mui/icons-material/Handyman";
import DeleteIcon from "@mui/icons-material/Delete";
import DirectionsCarIcon from "@mui/icons-material/DirectionsCar";
import PersonIcon from "@mui/icons-material/Person";
import DesignServicesIcon from "@mui/icons-material/DesignServices";
import EditActionDuringPatrol from "./EditActionDuringPatrol";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const ActionInfo = ({ action, setActionsList, index, deleteAction }) => {
return (
<Card sx={{ maxWidth: 300 }}>
<CardHeader
avatar={<HandymanIcon color="primary" sx={{ width: "30px", height: "30px" }} />}
title={<Typography sx={{ fontSize: "14px", fontWeight: 500 }}>{action.data.item_name}</Typography>}
subheader={action.data.sub_item_name}
/>
<CardContent>
<Stack sx={{ gap: 1 }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<DesignServicesIcon />
<Typography variant="button">میزان فعالیت انجام شده:</Typography>
<Typography variant="button">
{action.data.amount}({action.data.unit_fa})
</Typography>
</Box>
<Box>
<Divider sx={{ mb: 1 }}>
<Chip size="small" label="خودرو ها" icon={<DirectionsCarIcon />} />
</Divider>
<Stack>
{action.data.cmms_machines.map((cmms_machine, index) => (
<Typography variant="button" key={index}>
{index + 1}: {cmms_machine.car_name}
</Typography>
))}
</Stack>
</Box>
<Box>
<Divider sx={{ mb: 1 }}>
<Chip size="small" label="راهداران" icon={<PersonIcon />} />
</Divider>
<Stack>
{action.data.rahdaran_id.map((rahdar, index) => (
<Typography variant="button" key={index}>
{index + 1}: {rahdar.name}
</Typography>
))}
</Stack>
</Box>
</Stack>
</CardContent>
<CardActions disableSpacing>
<Box>
<IconButton aria-label="حذف فعالیت" color="error" onClick={deleteAction}>
<DeleteIcon />
</IconButton>
<EditActionDuringPatrol action={action} index={index} setActionsList={setActionsList} />
</Box>
</CardActions>
</Card>
);
};
export default ActionInfo;

View File

@@ -0,0 +1,90 @@
"use client";
import { Box, Button, Divider, Grid, Typography } from "@mui/material";
import AddCircleIcon from "@mui/icons-material/AddCircle";
import React, { useState } from "react";
import ModalActionsDuringPatrol from "./ModalActionsDuringPatrol";
import ActionInfo from "./ActionInfo";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
const ActionsDuringPatrol = ({ setValue, tabState, setTabState }) => {
const [ActionsList, setActionsList] = useState([]);
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const deleteAction = (indexToRemove) => {
setActionsList((prev) => prev.filter((_, index) => index !== indexToRemove));
};
const SubmitActionDuringPatrol = () => {
setTabState(tabState + 1);
const resultItems = ActionsList.map((item) => item.result);
console.log("resultItems", resultItems);
setValue("observed_items", resultItems);
};
return (
<Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Typography variant="h6">لیست اقدامات حین گشت</Typography>
<Button
onClick={handleOpen}
startIcon={<AddCircleIcon />}
variant="outlined"
sx={{ mb: 1, alignSelf: "end" }}
>
ثبت اقدام
</Button>
<ModalActionsDuringPatrol open={open} setOpen={setOpen} setActionsList={setActionsList} />
</Box>
<Divider />
{ActionsList.length !== 0 ? (
<Grid
container
spacing={2}
sx={{
p: 1,
mt: 1,
maxHeight: "300px",
overflowY: "auto",
}}
>
{ActionsList.map((action, index) => (
<Grid key={index} item xs={12} sm={6} lg={4}>
<ActionInfo
action={action}
setActionsList={setActionsList}
index={index}
deleteAction={() => deleteAction(index)}
/>
</Grid>
))}
</Grid>
) : (
<Box sx={{ my: 5, width: "100%", textAlign: "center" }}>
<Typography variant="h6" sx={{ letterSpacing: "2px", color: "#606060" }}>
فعالیتی ثبت نکرده اید
</Typography>
</Box>
)}
<Box>
<Box sx={{ display: "flex", gap: 1, justifyContent: "center" }}>
<Button
variant="contained"
color="primary"
onClick={SubmitActionDuringPatrol}
disabled={ActionsList === []}
endIcon={<KeyboardDoubleArrowLeftIcon />}
sx={{ mt: 2 }}
>
نهایی کردن درخواست
</Button>
</Box>
</Box>
</Box>
);
};
export default ActionsDuringPatrol;

View File

@@ -0,0 +1,229 @@
"use client";
import React, { useEffect, useState } from "react";
import {
Box,
Button,
Chip,
Divider,
FormControl,
Grid,
InputLabel,
OutlinedInput,
Slide,
Stack,
Typography,
} from "@mui/material";
import BeenhereIcon from "@mui/icons-material/Beenhere";
import ForwardToInboxIcon from "@mui/icons-material/ForwardToInbox";
import { formatCounter } from "@/core/utils/formatCounter";
import useRequest from "@/lib/hooks/useRequest";
import { GET_OTP_TOKEN } from "@/core/utils/routes";
import moment from "jalali-moment";
const CompeleteRequest = ({ watch, isSubmitting, setValue, register }) => {
const requestServer = useRequest({ auth: true });
const [delayPerRequest, setDelayPerRequest] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [validPhone, setValidPhone] = useState(false);
const [verificationCode, setVerificationCode] = useState("");
const [counter, setCounter] = useState(120);
const [otpSended, setOtpSended] = useState(false);
const [readyToVerify, setReadyToVerify] = useState(false);
const handleNumericInput = (e) => {
e.target.value = e.target.value.replace(/[^0-9]/g, "");
};
useEffect(() => {
if (phoneNumber.length === 11) {
setValidPhone(true);
} else {
setValidPhone(false);
}
if (verificationCode.length === 6 && phoneNumber.length === 11) {
setReadyToVerify(true);
} else {
setReadyToVerify(false);
}
}, [phoneNumber, verificationCode]);
useEffect(() => {
if (counter === 0) {
setDelayPerRequest(false);
return;
}
if (delayPerRequest && counter > 0) {
const timer = setInterval(() => {
setCounter((prevCounter) => prevCounter - 1);
}, 1000);
return () => clearInterval(timer);
}
}, [counter, delayPerRequest]);
const sendOtp = () => {
if (!validPhone) return false;
requestServer(`${GET_OTP_TOKEN}?phone_number=${phoneNumber}`, "get")
.then((response) => {
setOtpSended(true);
setDelayPerRequest(true);
})
.catch(() => {});
};
return (
<Stack>
<Stack>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Box>
<Typography sx={{ color: "#777777", fontWeight: 500 }} variant="body1">
{moment(watch("start_time")).locale("fa").format("YYYY/MM/DD | HH:mm")}
</Typography>
</Box>
<Box>
<Typography sx={{ color: "#777777", fontWeight: 500 }} variant="body1">
{moment(watch("end_time")).locale("fa").format("YYYY/MM/DD | HH:mm")}
</Typography>
</Box>
</Box>
<Grid container spacing={2} sx={{ alignItems: "start", my: 1 }}>
<Grid item xs={12} lg={6} sx={{ display: "flex", alignItems: "center" }}>
<Chip label="مدت زمان روشن بودن خودرو" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("vehicle_runtime")}</Typography>
</Grid>
<Grid item xs={12} lg={6} sx={{ display: "flex", alignItems: "center" }}>
<Chip label="میزان سوخت مصرفی" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("fuel_consumption")}</Typography>
</Grid>
<Grid item xs={12} lg={6} sx={{ display: "flex", alignItems: "center" }}>
<Chip label="تعداد نقاط توقف" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("stop_points").length}</Typography>
</Grid>
<Grid item xs={12} lg={6} sx={{ display: "flex", alignItems: "center" }}>
<Chip label="مسافت پیموده شده" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("distance")}</Typography>
</Grid>
<Grid item xs={12} lg={6} sx={{ display: "flex", alignItems: "center" }}>
<Chip label="تعداد فعالیت های حین گشت" />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("observed_items").length}</Typography>
</Grid>
</Grid>
<Grid container spacing={3} sx={{ alignItems: "start", my: 2 }}>
<Grid item xs={12} lg={6}>
<Divider>
<Chip label="خودرو گشت" />
</Divider>
<Box sx={{ display: "flex", alignItems: "center", my: 1 }}>
<Typography>کد</Typography>
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("road_patrol_machines_id.machine_code")}</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", my: 1 }}>
<Typography>نام</Typography>
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("road_patrol_machines_id.car_name")}</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", my: 1 }}>
<Typography>پلاک</Typography>
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>{watch("road_patrol_machines_id.plak_number")}</Typography>
</Box>
</Grid>
<Grid item xs={12} lg={6}>
<Divider>
<Chip label="راهدار / راهداران حاضر در گشت" />
</Divider>
{watch("road_patrol_rahdaran_id")?.map((rahdar, index) => (
<Box sx={{ display: "flex", alignItems: "center", my: 1 }}>
<Typography>نام و کد راهدار</Typography>
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography>
{rahdar.name} | {rahdar.code}
</Typography>
</Box>
))}
</Grid>
</Grid>
<Box sx={{ display: "flex", gap: 2, my: 2, justifyContent: "space-around" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<FormControl size="small" fullWidth variant="outlined">
<InputLabel htmlFor="phone_number">شماره تلفن مامور گشت</InputLabel>
<OutlinedInput
id="phone_number"
label={"شماره تلفن مامور گشت"}
disabled={delayPerRequest}
autoComplete="off"
size="small"
fullWidth
{...register("phone_number")}
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
onInput={handleNumericInput}
type="text"
inputProps={{
maxLength: 11,
}}
/>
</FormControl>
<Button
onClick={sendOtp}
disabled={!validPhone || delayPerRequest}
variant="contained"
color="success"
sx={{ textWrap: "nowrap", px: 4 }}
endIcon={<ForwardToInboxIcon />}
>
{delayPerRequest ? formatCounter(counter) : "دریافت کد"}
</Button>
</Box>
<Slide in={otpSended}>
<Box>
<FormControl size="small" fullWidth variant="outlined" focused>
<InputLabel htmlFor="verification_code">کد پیامک شده</InputLabel>
<OutlinedInput
id="verification_code"
label={"کد پیامک شده"}
placeholder="------"
autoComplete="off"
size="small"
fullWidth
{...register("verification_code")}
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
onInput={handleNumericInput}
type="text"
inputProps={{
maxLength: 6,
inputMode: "numeric",
style: {
textAlign: "center",
letterSpacing: "10px",
},
}}
/>
</FormControl>
</Box>
</Slide>
</Box>
</Stack>
<Button
variant="contained"
size="large"
disabled={isSubmitting}
type={"submit"}
form="patrol_form"
endIcon={<BeenhereIcon />}
>
{isSubmitting ? "در حال ثبت فعالیت" : "ثبت فعالیت"}
</Button>
</Stack>
);
};
export default CompeleteRequest;

View File

@@ -0,0 +1,57 @@
"use client";
import React, { useState } from "react";
import dynamic from "next/dynamic";
import MapLoading from "@/core/components/MapLayer/Loading";
import { Dialog, DialogTitle, IconButton } from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import EditFormContent from "@/components/dashboard/roadItems/operator/RowActions/EditForm/EditFormContent";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const EditActionDuringPatrol = ({ action, index, setActionsList }) => {
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const HandleSubmit = async (newData) => {
setActionsList((prev) => {
const updatedList = [...prev];
updatedList[index] = {
...updatedList[index],
data: newData.data,
result: newData.result,
};
return updatedList;
});
setOpen(false);
};
return (
<>
<IconButton aria-label="ویرایش فعالیت" color="primary" onClick={handleOpen}>
<EditIcon />
</IconButton>
<Dialog
maxWidth="sm"
fullWidth
open={open}
PaperProps={{
sx: {
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
},
}}
>
<DialogTitle>ویرایش اطلاعات</DialogTitle>
<EditFormContent setOpenEditDialog={setOpen} onSubmit={HandleSubmit} defaultData={action.data} />
</Dialog>
</>
);
};
export default EditActionDuringPatrol;

View File

@@ -0,0 +1,16 @@
import CreateFormContent from "@/components/dashboard/roadItems/operator/Actions/Create/Forms/CreateFormContent";
import { Dialog } from "@mui/material";
const ModalActionsDuringPatrol = ({ open, setOpen, setActionsList }) => {
const HandleSubmit = async (data) => {
setActionsList((prev) => [...prev, data]);
setOpen(false);
};
return (
<Dialog open={open} fullWidth maxWidth="sm">
<CreateFormContent setOpen={setOpen} onSubmit={HandleSubmit} is_gasht={true} />
</Dialog>
);
};
export default ModalActionsDuringPatrol;

View File

@@ -0,0 +1,203 @@
"use client";
import { Box, Button, Chip, Divider, IconButton, InputAdornment, Stack } from "@mui/material";
import React, { useEffect, useState } from "react";
import CarCode from "@/core/components/CarCode";
import SearchIcon from "@mui/icons-material/Search";
import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
import { faIR } from "@mui/x-date-pickers/locales";
import { LocalizationProvider, MobileDateTimePicker } from "@mui/x-date-pickers";
import ClearIcon from "@mui/icons-material/Clear";
import moment from "jalali-moment";
import dynamic from "next/dynamic";
import MapLoading from "@/core/components/MapLayer/Loading";
import { Controller } from "react-hook-form";
import { GET_FMS_DATA } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import PatrolDetailInfo from "./PatrolDetailInfo";
import PatrolResultCodeErrors from "./PatrolResultCodeErrors";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const PatrolDetail = ({ control, watch, setValue, tabState, setTabState }) => {
const requestServer = useRequest({ notificationSuccess: true });
const [patrolResultStatus, setPatrolResultStatus] = useState(null);
const [readyToRequest, setReadyToRequest] = useState(false);
const [patrolData, setPatrolData] = useState(null);
const requestPatrolInfo = async () => {
const formData = new FormData();
formData.append("machineCode", watch("road_patrol_machines_id").machine_code);
formData.append("startDT", moment(watch("start_time")).format("YYYY-MM-DDTHH:mm"));
formData.append("endDT", moment(watch("end_time")).format("YYYY-MM-DDTHH:mm"));
await requestServer(GET_FMS_DATA, "post", {
data: formData,
})
.then((response) => {
const data = response.data.data;
setPatrolData({
...data,
roadPatrolMachinesId: watch("road_patrol_machines_id"),
start_time: watch("start_time"),
end_time: watch("end_time"),
});
setPatrolResultStatus(data.resultCode);
})
.catch((error) => {});
};
useEffect(() => {
const roadPatrolMachinesId = watch("road_patrol_machines_id");
const startTime = watch("start_time");
const endTime = watch("end_time");
const isReady = roadPatrolMachinesId !== null && startTime !== "" && endTime !== "";
setReadyToRequest(isReady);
}, [watch("road_patrol_machines_id"), watch("start_time"), watch("end_time")]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
my: 3,
}}
>
<Box sx={{ display: "flex", flexDirection: { xs: "column", lg: "row" }, gap: 2 }}>
<Stack sx={{ minWidth: "200px" }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<CarCode
carCode={field.value}
setCarCode={(value) => field.onChange(value)}
error={error}
/>
);
}}
name={"road_patrol_machines_id"}
/>
</Stack>
<Box sx={{ display: "flex", gap: 2 }}>
<LocalizationProvider
dateAdapter={AdapterDateFnsJalali}
localeText={faIR.components.MuiLocalizationProvider.defaultProps.localeText}
>
<MobileDateTimePicker
value={watch("start_time") ? new Date(watch("start_time")) : null}
ampm={false}
name="تاریخ و ساعت شروع گشت"
closeOnSelect
maxDateTime={watch("end_time") ? new Date(watch("end_time")) : null}
onChange={(start_time) => {
const date = new Date(start_time);
const formattedDate = moment(date).locale("en").format("YYYY-MM-DD HH:mm");
setValue("start_time", formattedDate);
}}
slotProps={{
textField: {
size: "small",
placeholder: "تاریخ و ساعت شروع گشت",
InputProps: {
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={(event) => {
event.stopPropagation();
setValue("start_time", "");
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
},
}}
label="تاریخ و ساعت شروع گشت"
/>
<MobileDateTimePicker
value={watch("end_time") ? new Date(watch("end_time")) : null}
ampm={false}
name="تاریخ و ساعت پایان گشت"
closeOnSelect
minDateTime={watch("start_time") ? new Date(watch("start_time")) : null}
onChange={(end_time) => {
const date = new Date(end_time);
const formattedDate = moment(date).locale("en").format("YYYY-MM-DD HH:mm");
setValue("end_time", formattedDate);
}}
slotProps={{
textField: {
size: "small",
placeholder: "تاریخ و ساعت پایان گشت",
InputProps: {
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={(event) => {
event.stopPropagation();
setValue("end_time", "");
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
},
}}
label="تاریخ و ساعت پایان گشت"
/>
</LocalizationProvider>
</Box>
<Button
variant="contained"
onClick={requestPatrolInfo}
color="success"
disabled={!readyToRequest}
endIcon={<SearchIcon />}
>
درخواست اطلاعات
</Button>
</Box>
<Divider sx={{ my: 2, width: "100%" }}>
<Chip color="success" label="مشخصات گشت" />
</Divider>
{patrolResultStatus === 0 ? (
<PatrolDetailInfo
patrolData={patrolData}
tabState={tabState}
setTabState={setTabState}
setValue={setValue}
/>
) : (
<PatrolResultCodeErrors ResultCode={patrolResultStatus} />
)}
</Box>
);
};
export default PatrolDetail;

View File

@@ -0,0 +1,177 @@
"use client";
import { Box, Button, Card, CardActions, CardContent, Chip, Divider, Slide, Stack, Typography } from "@mui/material";
import React, { useEffect } from "react";
import dynamic from "next/dynamic";
import MapLoading from "@/core/components/MapLayer/Loading";
import LocalGasStationIcon from "@mui/icons-material/LocalGasStation";
import DirectionsCarFilledIcon from "@mui/icons-material/DirectionsCarFilled";
import WatchLaterIcon from "@mui/icons-material/WatchLater";
import QueryBuilderIcon from "@mui/icons-material/QueryBuilder";
import AddRoadIcon from "@mui/icons-material/AddRoad";
import ShareLocationIcon from "@mui/icons-material/ShareLocation";
import ElectricCarIcon from "@mui/icons-material/ElectricCar";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
import QrCode2Icon from "@mui/icons-material/QrCode2";
import moment from "jalali-moment";
import PatrolMapFeatures from "./PatrolMapFeatures";
import { useMap } from "react-leaflet";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const MapItemViewBound = ({ patrolData, bound }) => {
const map = useMap();
useEffect(() => {
if (bound.length !== 0) {
map.fitBounds(bound, { paddingTopLeft: [16, 16], paddingBottomRight: [16, 130] });
}
}, [bound]);
return (
<>
{patrolData.stopPoints.map((stopPoint, index) => (
<React.Fragment key={index}>
<PatrolMapFeatures stopPoint={stopPoint} />
</React.Fragment>
))}
</>
);
};
const PatrolDetailInfo = ({ patrolData, tabState, setTabState, setValue }) => {
const bound = patrolData.stopPoints.map((point) => [point.latitude, point.longitude]);
const SendPatrolInfo = () => {
setValue("vehicle_runtime", patrolData.accOnDuration);
setValue("fuel_consumption", patrolData.fuelConsumption);
setValue("distance", patrolData.mileage);
setValue("stop_points", patrolData.stopPoints);
setTabState(tabState + 1);
};
return (
<Slide in={true} sx={{ width: "100%" }}>
<Card>
<CardContent
sx={{
display: "flex",
flexDirection: { xs: "column", lg: "row" },
alignItems: "center",
justifyContent: "space-between",
gap: { xs: 2, lg: 0 },
}}
>
<Stack sx={{ minWidth: { xs: "100%", lg: "300px" }, gap: 2 }}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="مدت زمان روشن بودن خودرو" icon={<ElectricCarIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{Math.floor(patrolData.accOnDuration / 60)} دقیقه
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="نام خودرو" icon={<DirectionsCarFilledIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{patrolData.roadPatrolMachinesId.car_name}
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="کد خودرو" icon={<QrCode2Icon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{patrolData.roadPatrolMachinesId.machine_code}
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="پلاک خودرو" icon={<DirectionsCarFilledIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{patrolData.roadPatrolMachinesId.plak_number}
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="زمان شروع گشت" icon={<QueryBuilderIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{moment(patrolData.start_time).locale("fa").format("HH:mm | YYYY/MM/DD")}
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="زمان پایان گشت" icon={<WatchLaterIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{moment(patrolData.end_time).locale("fa").format("HH:mm | YYYY/MM/DD")}
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="مسافت" icon={<AddRoadIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{(patrolData.mileage / 1000).toFixed(1)} کیلومتر
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="میزان مصرف سوخت" icon={<LocalGasStationIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{Math.round(patrolData.fuelConsumption * 10) / 10} لیتر
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Chip size="small" label="تعداد توقف در مسیر" icon={<ShareLocationIcon />} />
<Divider sx={{ flex: 1, mx: 2 }} />
<Typography sx={{ fontSize: "13px", fontWeight: 400 }}>
{patrolData.stopPoints.length} مورد
</Typography>
</Box>
</Stack>
<Divider orientation="vertical" flexItem />
<Stack spacing={1} sx={{ height: "300px", width: { xs: "100%", lg: "350px" } }}>
<Box
sx={{
p: 1,
border: "1px dashed",
borderColor: "divider",
borderRadius: 1,
height: "100%",
}}
>
<Box
sx={{
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MapLayer position={bound}>
<MapItemViewBound patrolData={patrolData} bound={bound} />
</MapLayer>
</Box>
</Box>
</Stack>
</CardContent>
<CardActions sx={{ alignItems: "center", justifyContent: "center" }}>
<Box sx={{ display: "flex", gap: 1 }}>
<Button
variant="contained"
color="primary"
onClick={SendPatrolInfo}
endIcon={<KeyboardDoubleArrowLeftIcon />}
sx={{ mt: 2 }}
>
تایید اطلاعات و رفتن به مرحله بعد
</Button>
</Box>
</CardActions>
</Card>
</Slide>
);
};
export default PatrolDetailInfo;

View File

@@ -0,0 +1,179 @@
"use client";
import { Box, DialogContent, Tab, Tabs, useMediaQuery } from "@mui/material";
import { useTheme } from "@emotion/react";
import React, { useEffect, useState } from "react";
import TaxiAlertIcon from "@mui/icons-material/TaxiAlert";
import EngineeringIcon from "@mui/icons-material/Engineering";
import HandymanIcon from "@mui/icons-material/Handyman";
import VerifiedIcon from "@mui/icons-material/Verified";
import PatrolTimeLine from "./PatrolTimeLine";
import PatrolDetail from "./PatrolDetail";
import SuperVisorsDetail from "./SuperVisorsDetail";
import ActionsDuringPatrol from "./ActionsDurigPatrol";
import { useForm } from "react-hook-form";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import CompeleteRequest from "./CompeleteRequest";
function TabPanel(props) {
const { children, value, index } = props;
return (
<div role="tabpanel" hidden={value !== index}>
{value === index && <Box>{children}</Box>}
</div>
);
}
const defaultValues = {
road_patrol_rahdaran_id: null,
road_patrol_machines_id: null,
start_time: "",
end_time: "",
stop_points: null,
vehicle_runtime: "",
fuel_consumption: "",
distance: "",
observed_items: null,
phone_number: "",
otp_token: "",
};
const PatrolForms = ({ mutate, setOpen }) => {
const requestServer = useRequest({ notificationSuccess: true });
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const [tabState, setTabState] = useState(0);
const [activeUpTo, setActiveUpTo] = useState(0);
const handleChangeTab = (event, newValue) => {
setTabState(newValue);
};
useEffect(() => {
if (activeUpTo < tabState) {
setActiveUpTo(tabState);
}
}, [tabState]);
const {
control,
watch,
getValues,
register,
handleSubmit,
setValue,
resetField,
formState: { isSubmitting },
} = useForm({
defaultValues,
mode: "onBlur",
});
const onSubmit = async (data) => {
console.log("data", data);
// const formData = new FormData();
// data.road_patrol_rahdaran_id.forEach((rahdar, index) => formData.append(`road_patrol_rahdaran_id[${index}]`, rahdar.id));
// formData.append(`road_patrol_machines_id[0]`, data.road_patrol_machines_id.id)
// data.stop_points.forEach((stop_point, index) => formData.append(`stop_points[${index}]`, stop_point))
// data.observed_items.forEach((observed_item, index) => {
// formData.append(`observed_items[${index}][instant_action]`, 1);
// formData.append(`observed_items[${index}][local_name]`, "");
// formData.append(`observed_items[${index}][start_point]`, observed_item.start_point);
// formData.append(`observed_items[${index}][start_point]`, observed_item.end_point);
// formData.append(`observed_items[${index}][amount]`, observed_item.amount);
// formData.append(`observed_items[${index}][before_image]`, observed_item.before_image);
// formData.append(`observed_items[${index}][after_image]`, observed_item.after_image);
// observed_item.cmms_machines.forEach((machine) => formData.append(`observed_items[${index}][cmms_machines][]`, machine.id))
// observed_item.rahdaran_id.forEach((rahdar) => formData.append(`observed_items[${index}][rahdaran_id][]`, rahdar.id))
// formData.append(`observed_items[${index}][sub_item_id]`, observed_item.sub_item_id);
// })
// formData.append("start_time", data.start_time);
// formData.append("end_time", data.end_time);
// formData.append("vehicle_runtime", data.vehicle_runtime);
// formData.append("fuel_consumption", +data.fuel_consumption);
// formData.append("distance", data.distance);
// formData.append("description", "");
// formData.append("phone_number", data.phone_number);
// formData.append("verification_code", data.verification_code);
// if (data.phone_number !== "" && data.verification_code !== "") {
// try {
// await requestServer(CREATE_PATROL, "post", {
// data: formData,
// });
// mutate();
// setOpen(false);
// } catch (error) {
// }
// } else {
// console.log("error phone number")
// }
};
return (
<StyledForm onSubmit={handleSubmit(onSubmit)} id="patrol_form">
<Tabs
allowScrollButtonsMobile
value={tabState}
onChange={handleChangeTab}
variant={`${isMobile ? "scrollable" : "fullWidth"}`}
sx={{
display: "flex",
justifyContent: "space-around",
width: "100%",
backgroundColor: "#efefef",
}}
>
<Tab disabled={!(activeUpTo >= 0)} icon={<TaxiAlertIcon />} label="مشخصات گشت"></Tab>
<Tab disabled={!(activeUpTo >= 1)} icon={<EngineeringIcon />} label="مشخصات راهداران"></Tab>
<Tab disabled={!(activeUpTo >= 2)} icon={<HandymanIcon />} label="اقدامات حین گشت"></Tab>
<Tab disabled={!(activeUpTo >= 3)} icon={<VerifiedIcon />} label="تکمیل درخواست"></Tab>
</Tabs>
<DialogContent dividers sx={{ display: "flex" }}>
<Box sx={{ flex: 1 }}>
<TabPanel value={tabState} index={0}>
<PatrolDetail
control={control}
setValue={setValue}
watch={watch}
tabState={tabState}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={1}>
<SuperVisorsDetail
control={control}
setValue={setValue}
watch={watch}
tabState={tabState}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={2}>
<ActionsDuringPatrol
watch={watch}
setValue={setValue}
tabState={tabState}
setTabState={setTabState}
/>
</TabPanel>
<TabPanel value={tabState} index={3}>
<CompeleteRequest
register={register}
setValue={setValue}
watch={watch}
isSubmitting={isSubmitting}
/>
</TabPanel>
</Box>
{!isMobile && (
<Box sx={{ display: "flex", alignItems: "center" }}>
<PatrolTimeLine tabState={tabState} />
</Box>
)}
</DialogContent>
</StyledForm>
);
};
export default PatrolForms;

View File

@@ -0,0 +1,38 @@
"use client";
import { Typography } from "@mui/material";
import React, { useRef } from "react";
import { Marker, Popup } from "react-leaflet";
import AzmayeshIcon from "@/assets/images/examine_marker.png";
const PatrolMapFeatures = ({ stopPoint }) => {
const position = [stopPoint.latitude, stopPoint.longitude];
const mapPatrolMarker = useRef();
const createCustomIcon = (size, iconUrl) => {
return L.icon({
iconUrl: iconUrl,
iconSize: size,
iconAnchor: [size[0] / 2, size[1]],
popupAnchor: [0, -size[1]],
});
};
return (
<Marker
ref={mapPatrolMarker}
icon={L.icon({
iconUrl: AzmayeshIcon.src,
iconSize: [35, 35],
iconAnchor: [35 / 2, 35],
popupAnchor: [0, -35],
})}
position={position}
>
<Popup>
<Typography variant="button">مدت زمان توقف: {stopPoint.duration} ثانیه</Typography>
</Popup>
</Marker>
);
};
export default PatrolMapFeatures;

View File

@@ -0,0 +1,25 @@
"use client";
import { Box, Typography } from "@mui/material";
import React from "react";
const PatrolResultCodeErrors = ({ ResultCode }) => {
const errorMessages = {
[-1]: "نام کاربری یا کلمه عبور صحیح نمیباشد",
[-2]: "ردیاب به خودرو انتخابی متصل نیست",
[-3]: "کاربر دسترسی لازم به اطلاعات خودرو انتخابی را ندارد",
[-4]: "خطای درخواست مکرر",
};
const message = errorMessages[ResultCode] || "ابتدا اطلاعات بالا را تکمیل نمایید";
return (
<Box sx={{ my: 5 }}>
<Typography variant="h6" sx={{ letterSpacing: "2px", color: ResultCode ? "error.main" : "#606060" }}>
{message}
</Typography>
</Box>
);
};
export default PatrolResultCodeErrors;

View File

@@ -0,0 +1,119 @@
"use client";
import { Box, CircularProgress, Typography } from "@mui/material";
import TaxiAlertIcon from "@mui/icons-material/TaxiAlert";
import EngineeringIcon from "@mui/icons-material/Engineering";
import HandymanIcon from "@mui/icons-material/Handyman";
import VerifiedIcon from "@mui/icons-material/Verified";
import {
Timeline,
TimelineConnector,
TimelineContent,
TimelineDot,
TimelineItem,
timelineItemClasses,
TimelineSeparator,
} from "@mui/lab";
const PatrolTimeLine = ({ tabState }) => {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Timeline
position="right"
sx={{
[`& .${timelineItemClasses.root}:before`]: { flex: 0, padding: 0 },
}}
>
<TimelineItem>
<TimelineSeparator>
<TimelineConnector />
<TimelineDot sx={{ backgroundColor: "#fff" }}>
{tabState === 0 ? (
<CircularProgress size="23px" />
) : (
<TaxiAlertIcon
color={tabState > 0 ? "success" : "error"}
sx={{ width: "1.5rem", height: "1.5rem" }}
/>
)}
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<TimelineContent sx={{ py: "12px", px: 2 }}>
<Typography variant="h6">مشخصات گشت</Typography>
<Typography variant="caption" sx={{ color: "#606060" }}>
تایید اطلاعات اولیه گشت
</Typography>
</TimelineContent>
</TimelineItem>
<TimelineItem>
<TimelineSeparator>
<TimelineConnector />
<TimelineDot sx={{ backgroundColor: "#fff" }}>
{tabState === 1 ? (
<CircularProgress size="23px" />
) : (
<EngineeringIcon
color={tabState > 1 ? "success" : "error"}
sx={{ width: "1.5rem", height: "1.5rem" }}
/>
)}
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<TimelineContent sx={{ py: "12px", px: 2 }}>
<Typography variant="h6">مشخصات راهداران</Typography>
<Typography variant="caption" sx={{ color: "#606060" }}>
راهدار / راهداران حاضر در گشت
</Typography>
</TimelineContent>
</TimelineItem>
<TimelineItem>
<TimelineSeparator>
<TimelineConnector />
<TimelineDot sx={{ backgroundColor: "#fff" }}>
{tabState === 2 ? (
<CircularProgress size="23px" />
) : (
<HandymanIcon
color={tabState > 2 ? "success" : "error"}
sx={{ width: "1.5rem", height: "1.5rem" }}
/>
)}
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<TimelineContent sx={{ py: "12px", px: 2 }}>
<Typography variant="h6">اقدامات حین گشت</Typography>
<Typography variant="caption" sx={{ color: "#606060" }}>
ثبت فعالیت های صورت گرفته حین گشت
</Typography>
</TimelineContent>
</TimelineItem>
<TimelineItem>
<TimelineSeparator>
<TimelineConnector />
<TimelineDot sx={{ backgroundColor: "#fff" }}>
{tabState === 3 ? (
<CircularProgress size="23px" />
) : (
<VerifiedIcon
color={tabState > 3 ? "success" : "error"}
sx={{ width: "1.5rem", height: "1.5rem" }}
/>
)}
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<TimelineContent sx={{ py: "12px", px: 2 }}>
<Typography variant="h6">تکمیل درخواست</Typography>
<Typography variant="caption" sx={{ color: "#606060" }}>
تکمیل درخواست و ثبت نهایی
</Typography>
</TimelineContent>
</TimelineItem>
</Timeline>
</Box>
);
};
export default PatrolTimeLine;

View File

@@ -0,0 +1,118 @@
"use client";
import { Box, Button, FormControl, InputLabel, OutlinedInput } from "@mui/material";
import React, { useEffect, useState } from "react";
import ForwardToInboxIcon from "@mui/icons-material/ForwardToInbox";
import useRequest from "@/lib/hooks/useRequest";
import { GET_OTP_TOKEN, VERIFY_OTP } from "@/core/utils/routes";
import { formatCounter } from "@/core/utils/formatCounter";
const PhoneValidation = ({ tabState, setTabState }) => {
const requestServer = useRequest({ auth: true });
const [phoneNumber, setPhoneNumber] = useState("");
const [verificationCode, setVerificationCode] = useState("");
const [validPhone, setValidPhone] = useState(false);
const [delayPerRequest, setDelayPerRequest] = useState(false);
const [counter, setCounter] = useState(120);
const [otpSended, setOtpSended] = useState(false);
const [readyToVerify, setReadyToVerify] = useState(false);
const handleNumericInput = (e) => {
e.target.value = e.target.value.replace(/[^0-9]/g, "");
};
useEffect(() => {
if (phoneNumber.length === 11) {
setValidPhone(true);
} else {
setValidPhone(false);
}
if (verificationCode.length === 6 && phoneNumber.length === 11) {
setReadyToVerify(true);
} else {
setReadyToVerify(false);
}
}, [phoneNumber, verificationCode]);
useEffect(() => {
if (counter === 0) {
setDelayPerRequest(false);
return;
}
if (delayPerRequest && counter > 0) {
const timer = setInterval(() => {
setCounter((prevCounter) => prevCounter - 1);
}, 1000);
return () => clearInterval(timer);
}
}, [counter, delayPerRequest]);
const sendOtp = () => {
if (!validPhone) return false;
requestServer(`${GET_OTP_TOKEN}?phone_number=${phoneNumber}`, "get")
.then((response) => {})
.catch(() => {});
//////////**** (mohammad) this part should keep in success but for now we go next level ****////////////
setOtpSended(true);
setDelayPerRequest(true);
};
const phoneRegister = () => {
const formData = new FormData();
formData.append("phone_number", phoneNumber);
formData.append("verification_code", verificationCode);
setTabState(tabState + 1);
requestServer(VERIFY_OTP, "post", {
data: formData,
})
.then(() => {})
.catch(() => {});
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
my: 3,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2, width: { xs: "100%", sm: "70%" } }}>
<FormControl size="small" fullWidth variant="outlined">
<InputLabel htmlFor="phone_number">شماره تلفن مامور گشت</InputLabel>
<OutlinedInput
id="phone_number"
label={"شماره تلفن مامور گشت"}
disabled={delayPerRequest}
autoComplete="off"
size="small"
fullWidth
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
onInput={handleNumericInput}
type="text"
inputProps={{
maxLength: 11,
}}
/>
</FormControl>
<Button
onClick={sendOtp}
disabled={!validPhone || delayPerRequest}
variant="contained"
color="success"
sx={{ width: "200px", height: "37px" }}
endIcon={<ForwardToInboxIcon />}
>
{delayPerRequest ? formatCounter(counter) : "دریافت کد"}
</Button>
</Box>
</Box>
);
};
export default PhoneValidation;

View File

@@ -0,0 +1,38 @@
"use client";
import { Box, Card, IconButton, Stack, Typography } from "@mui/material";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import DeleteIcon from "@mui/icons-material/Delete";
import React from "react";
const SuperVisorInfo = ({ superVisor, deleteSuperVisor }) => {
return (
<Card
elevation={3}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 1,
py: 1,
}}
>
<Box sx={{ display: "flex" }}>
<AccountCircleIcon color="primary" sx={{ width: "50px", height: "50px" }} />
<Stack sx={{ ml: 1, justifyContent: "center" }}>
<Typography variant="body1" sx={{ letterSpacing: "1px", fontWeight: 500 }}>
{superVisor.name}
</Typography>
<Typography variant="caption">کد راهدار: {superVisor.code}</Typography>
</Stack>
</Box>
<Box>
<IconButton color="error" onClick={() => deleteSuperVisor(superVisor.id)}>
<DeleteIcon />
</IconButton>
</Box>
</Card>
);
};
export default SuperVisorInfo;

View File

@@ -0,0 +1,139 @@
"use client";
import { Box, Button, Chip, Divider, Grid, Stack, Typography } from "@mui/material";
import SaveAltIcon from "@mui/icons-material/SaveAlt";
import React, { useEffect, useState } from "react";
import RahdarCode from "@/core/components/RahdarCode";
import SuperVisorInfo from "@/components/dashboard/roadPatrols/operator/Forms/CreatePatrol/SuperVisorInfo";
import KeyboardDoubleArrowRightIcon from "@mui/icons-material/KeyboardDoubleArrowRight";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
import { Controller } from "react-hook-form";
const SuperVisorsDetail = ({ control, watch, setValue, tabState, setTabState }) => {
const [SuperVisorList, setSuperVisorList] = useState([]);
const [readyToRequest, setReadyToRequest] = useState(false);
const requestPatrolInfo = () => {
setSuperVisorList((prev) => [...prev, watch("road_patrol_rahdaran_id")]);
setReadyToRequest(false);
setValue("road_patrol_rahdaran_id", null);
};
const deleteSuperVisor = (id) => {
setSuperVisorList((prev) => prev.filter((superVisor) => superVisor.id !== id));
};
useEffect(() => {
setReadyToRequest(watch("road_patrol_rahdaran_id") != null);
}, [watch("road_patrol_rahdaran_id")]);
const SendSuperVisor = () => {
setValue("road_patrol_rahdaran_id", SuperVisorList);
setTabState(tabState + 1);
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
my: 3,
}}
>
<Grid container sx={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<Grid
item
xs={6}
sx={{ display: "flex", alignItems: "center", flexDirection: { xs: "column", lg: "row" }, gap: 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={"road_patrol_rahdaran_id"}
/>
</Stack>
<Button
variant="contained"
onClick={requestPatrolInfo}
color="success"
sx={{ textWrap: "nowrap", px: 3 }}
disabled={!readyToRequest}
endIcon={<SaveAltIcon />}
>
ثبت راهدار
</Button>
</Grid>
</Grid>
<Divider sx={{ my: 2, width: "100%" }}>
<Chip color="success" variant="outlined" label="لیست راهداران انتخاب شده" />
</Divider>
{SuperVisorList.length !== 0 ? (
<>
<Grid
container
spacing={2}
sx={{
p: 1,
mt: 1,
maxHeight: "300px",
overflowY: "auto",
}}
>
{SuperVisorList.map((superVisor) => (
<Grid key={superVisor.id} item xs={12} sm={6} lg={4}>
<SuperVisorInfo superVisor={superVisor} deleteSuperVisor={deleteSuperVisor} />
</Grid>
))}
</Grid>
<Box
sx={{
display: "flex",
gap: 1,
mt: 2,
width: "100%",
justifyContent: "center",
}}
>
<Button
variant="outlined"
color="error"
startIcon={<KeyboardDoubleArrowRightIcon />}
sx={{ mt: 2 }}
>
بازگشت
</Button>
<Button
variant="contained"
color="primary"
onClick={SendSuperVisor}
endIcon={<KeyboardDoubleArrowLeftIcon />}
sx={{ mt: 2 }}
>
تایید اطلاعات و رفتن به مرحله بعد
</Button>
</Box>
</>
) : (
<Box sx={{ my: 5, width: "100%", textAlign: "center" }}>
<Typography variant="h6" sx={{ letterSpacing: "2px", color: "#606060" }}>
ابتدا اطلاعات بالا را تکمیل نمایید
</Typography>
</Box>
)}
</Box>
);
};
export default SuperVisorsDetail;

View File

@@ -0,0 +1,13 @@
"use client";
import { Dialog } from "@mui/material";
import PatrolForms from "./PatrolForms";
const CreatePatrol = ({ open, setOpen, mutate }) => {
return (
<Dialog open={open} fullWidth maxWidth="lg">
<PatrolForms setOpen={setOpen} mutate={mutate} />
</Dialog>
);
};
export default CreatePatrol;

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import OperatorList from "@/components/dashboard/roadPatrols/operator/OperatorList";
import OperatorList from "./OperatorList";
const OperatorPage = () => {
return (

View File

@@ -1,13 +1,13 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import SupervisorList from "./SupervisorList";
import SuperviserList from "./SupervisorList";
const SupervisorPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی"} />
<SupervisorList />
<SuperviserList />
</Stack>
);
};

View File

@@ -0,0 +1,7 @@
export const formatCounter = (counter) => {
const minutes = Math.floor(counter / 60)
.toString()
.padStart(2, "0");
const seconds = (counter % 60).toString().padStart(2, "0");
return `${minutes}:${seconds}`;
};

View File

@@ -195,13 +195,6 @@ export const pageMenu = [
hasSubitems: true,
badges: ["road_patrols.operation_cnt"],
Subitems: [
{
id: "roadPatrolManagmentOparationCreate",
label: "ثبت اقدام",
type: "page",
route: "/dashboard/road-patrols/operator",
permissions: ["add-road-patrol"],
},
{
id: "roadPatrolManagmentOparationCartable",
label: "کارتابل",

View File

@@ -34,6 +34,9 @@ export const EXPORT_ROAD_PATROL_OPERATOR_LIST = "https://rms.witel.ir/v2/road_pa
export const GET_ROAD_PATROL_SUPERVISOR_LIST = api + "/api/v3/road_patrols/supervisor_index";
export const EXPORT_ROAD_PATROL_SUPERVISOR_LIST = "https://rms.witel.ir/v2/road_patrols/supervisor/cartable/report";
export const DELETE_ROAD_PATROL_SUPERVISOR = api + "/api/v3/road_patrols/delete";
export const CREATE_PATROL = api + "/api/v3/road_patrols/store";
export const GET_FMS_DATA = api + "/api/v3/fms_vehicle/get_activity";
export const GET_OTP_TOKEN = api + "/v2/get_otp_token";
// road items
export const GET_ROAD_ITEMS_SUPERVISOR_LIST = api + "/api/v3/road_items/supervisor_index";

View File

@@ -20,7 +20,6 @@ const useRequest = (initOptions) => {
return async (url = "", method = "get", options) => {
const mergedOptions = Object.assign({}, _options, options);
try {
const response = await axios({
url,