push rasad front on new repo

This commit is contained in:
2026-01-18 14:32:49 +03:30
commit 4fe6e70525
2139 changed files with 303150 additions and 0 deletions

View File

@@ -0,0 +1,572 @@
import { useContext, useEffect, useState } from "react";
import useUserProfile from "../../../authentication/hooks/useUserProfile";
import { useFormik } from "formik";
import { Yup } from "../../../../lib/yup/yup";
import { fixBase64 } from "../../../../utils/toBase64";
import { Grid } from "../../../../components/grid/Grid";
import {
Autocomplete,
Button,
Checkbox,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
// Paper,
Select,
TextField,
Typography,
Chip,
Box,
Stack,
} from "@mui/material";
// import LaunchIcon from "@mui/icons-material/Launch";
// import SettingsSuggestIcon from "@mui/icons-material/SettingsSuggest";
// import MarkAsUnreadIcon from "@mui/icons-material/MarkAsUnread";
import { ImageUpload } from "../../../../components/image-upload/ImageUpload";
import { getFaUserRole } from "../../../../utils/getFaUserRole";
import { useDispatch } from "react-redux";
import {
getTicketPermission,
getTicketUsersFromRole,
} from "../../services/get-ticket-permission";
import { AppContext } from "../../../../contexts/AppContext";
import { CreateTicketService } from "../../services/create-ticket";
import { sendResponseTicketService } from "../../services/send-response";
import { useNavigate } from "react-router-dom";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import DeleteIcon from "@mui/icons-material/Delete";
// import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import { styled } from "@mui/material/styles";
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
export const CreateTicket = ({ id, getMessages, fetchMessages }) => {
// const ticketTypes = [
// {
// title: "درخواست از مدیر",
// icon: <LaunchIcon fontSize="40%" />,
// color: "#797979",
// },
// {
// title: "مشکلات فنی",
// icon: <SettingsSuggestIcon fontSize="40%" />,
// color: "#797979",
// },
// {
// title: "پیشنهاد و انتقاد",
// icon: <MarkAsUnreadIcon fontSize="40%" />,
// color: "#797979",
// },
// ];
const isAdmin = () => {
if (
selectedRole === "CityOperator" ||
selectedRole === "ProvinceOperator" ||
selectedRole === "AdminX" ||
selectedRole === "Supporter" ||
selectedRole === "SuperAdmin"
) {
return true;
} else {
return false;
}
};
const [role] = useUserProfile();
const [selectedRole, setSelectedRole] = useState(role[0]);
const [value, setValue] = useState(isAdmin() ? "toRole" : "toUser");
const [openNotif] = useContext(AppContext);
const navigate = useNavigate();
// const handleChangeRadioButton = (event) => {
// formik.setFieldValue("roles", []);
// setValue(event.target.value);
// };
const dispatch = useDispatch();
const handleRoleChange = (event) => {
setSelectedRole(event.target.value);
};
const [isChecked, setChecked] = useState(false);
const handleCheckboxChange = () => {
setChecked(!isChecked);
};
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
formik.setFieldValue("uploadedFile", file);
}
};
const handleDeleteFile = () => {
formik.setFieldValue("uploadedFile", null);
};
const formik = useFormik({
initialValues: {
title: "",
text: "",
users: [],
roles: [],
image: "",
uploadedFile: null,
},
validationSchema: Yup.object({
title: Yup.string().required("عنوان تیکت ضروری است"),
text: Yup.string().required("متن تیکت ضروری است"),
}),
onSubmit: (values) => {
// console.log(values);
},
});
useEffect(() => {
formik.validateForm();
}, []);
useEffect(() => {
if (!isAdmin()) {
setValue("toUser");
}
}, [value, selectedRole]);
const [profileImages, setProfileImages] = useState([]);
const factorPaymentHandler = (imageList, addUpdateIndex) => {
if (imageList[0]) {
formik.setFieldValue("image", fixBase64(imageList[0]?.data_url));
}
setProfileImages(imageList);
};
const [permissionList, setPermissionList] = useState([]);
const [users, setUsers] = useState([]);
useEffect(() => {
dispatch(getTicketPermission({ role: selectedRole })).then((r) => {
setPermissionList(r.payload.data);
});
}, [selectedRole]);
useEffect(() => {
if (formik.values.roles.length && value === "toUser") {
dispatch(getTicketUsersFromRole({ role: formik.values.roles })).then(
(r) => {
setUsers(r.payload.data);
}
);
} else {
setUsers([]);
}
}, [formik.values.roles, value]);
const handleCheckboxChangeToRole = (event) => {
if (event.target.checked) {
setValue("toRole");
formik.setFieldValue("users", []);
formik.setFieldValue("roles", []);
}
};
const handleCheckboxChangeToUser = (event) => {
if (event.target.checked) {
setValue("toUser");
formik.setFieldValue("roles", []);
formik.setFieldValue("users", []);
}
};
return (
<Grid
container
alignItems="center"
justifyContent="center"
gap={2}
width="100%"
>
{/* <Grid item xs={12} container justifyContent="center">
<Box
display="flex"
justifyContent="center"
gap={2}
mb={3}
sx={{
"& > *": { width: "100px", height: "120px", flex: "0 0 auto" },
}}
>
{ticketTypes.map((item) => (
<Paper
key={item.title}
sx={{
p: 3,
textAlign: "center",
cursor: "pointer",
borderRadius: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: 150,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 4,
},
}}
>
<Box
sx={{
color: item.color,
mb: 2,
fontSize: "2.5rem",
}}
>
{item.icon}
</Box>
<Typography
variant="h6"
sx={{ fontWeight: "medium", fontSize: "12px" }}
>
{item.title}
</Typography>
</Paper>
))}
</Box>
</Grid> */}
{isNaN(id) && (
<Grid container justifyContent="center" xs={12}>
<Grid item xs={12} container justifyContent="center">
<Stack direction="row" spacing={4}>
<FormControlLabel
control={
<Checkbox
checked={value === "toRole"}
onChange={handleCheckboxChangeToRole}
disabled={!isAdmin()}
/>
}
label="ارسال به نقش"
/>
<FormControlLabel
control={
<Checkbox
checked={value === "toUser"}
onChange={handleCheckboxChangeToUser}
/>
}
label="ارسال به اشخاص"
/>
</Stack>
</Grid>
<Grid item xs={12} container justifyContent="center">
{permissionList?.roles?.length &&
(value === "toRole" ? isAdmin() : true) ? (
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="role-select-label">انتخاب نقش</InputLabel>
<Select
multiple={value !== "toUser"}
labelId="role-select-label"
value={formik.values.roles}
onChange={(event) => {
formik.setFieldValue("roles", event.target.value);
}}
fullWidth
>
{permissionList?.roles.map((roleItem, i) => (
<MenuItem key={i} value={roleItem}>
{getFaUserRole(roleItem)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
) : (
<Typography variant="body1" color="error">
نقش انتخابی اجازه ارسال تیکت ندارد!
</Typography>
)}
</Grid>
{value === "toUser" && (
<>
{users?.length ? (
<Grid item xs={12} mt={2} v>
<Autocomplete
multiple
id="tags-standard"
options={users}
getOptionLabel={(option) => option.fullname}
onChange={(event, value) => {
formik.setFieldValue("users", value);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="انتخاب کاربر"
/>
)}
/>
</Grid>
) : (
<Typography color="error" variant="body2">
موردی یافت نشد!
</Typography>
)}
</>
)}
</Grid>
)}
{role.length > 1 && isNaN(id) && (
<Grid item xs={12} container justifyContent="center">
<FormControl fullWidth>
<InputLabel id="role-select-label">انتخاب نقش</InputLabel>
<Select
labelId="role-select-label"
value={selectedRole}
onChange={handleRoleChange}
>
{role.map((roleItem, i) => (
<MenuItem key={i} value={roleItem}>
{getFaUserRole(roleItem)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}
{isNaN(id) && (
<Grid item xs={12} container justifyContent="center">
<TextField
id="title"
name="title"
label="موضوع"
value={formik.values.title}
onChange={formik.handleChange}
error={formik.touched.title && Boolean(formik.errors.title)}
helperText={formik.touched.title && formik.errors.title}
/>
</Grid>
)}
<Grid item xs={12} container justifyContent="center">
<TextField
fullWidth
id="text"
name="text"
label="پیام خود را وارد کنید"
multiline
rows={4}
value={formik.values.text}
onChange={formik.handleChange}
error={formik.touched.text && Boolean(formik.errors.text)}
helperText={formik.touched.text && formik.errors.text}
/>
</Grid>
<Grid item xs={12} mt={2}>
<Button
component="label"
variant="outlined"
startIcon={<CloudUploadIcon />}
sx={{ mb: 1 }}
>
پیوست فایل
<VisuallyHiddenInput type="file" onChange={handleFileUpload} />
</Button>
{formik.values.uploadedFile && (
<Box display="flex" alignItems="center" mt={1}>
<Chip
label={formik.values.uploadedFile.name}
onDelete={handleDeleteFile}
deleteIcon={<DeleteIcon />}
variant="outlined"
/>
<Typography variant="caption" ml={1}>
حجم: {(formik.values.uploadedFile.size / 1024 / 1024).toFixed(2)}{" "}
MB
</Typography>
</Box>
)}
{formik.values.uploadedFile?.size > 5 * 1024 * 1024 && (
<Typography color="error" variant="body2">
حداکثر حجم مجاز جهت ارسال فایل 5 مگابایت است!
</Typography>
)}
</Grid>
{isAdmin() && isNaN(id) && (
<Grid container xs={12}>
<FormControlLabel
control={
<Checkbox
disabled={
value === "toUser" &&
Array.isArray(formik.values.users) &&
formik.values.users.length === 1
}
size="small"
checked={isChecked}
onChange={handleCheckboxChange}
/>
}
label="فقط خواندنی"
/>
</Grid>
)}
<Grid
container
justifyContent="center"
style={{ marginTop: "16px" }}
gap={2}
>
<ImageUpload
onChange={factorPaymentHandler}
images={profileImages}
maxNumber={1}
title={"ارسال تصویر"}
/>
</Grid>
<Button
disabled={
formik.values.uploadedFile?.size > 5 * 1024 * 1024 ||
(!isNaN(id)
? !formik.values.text
: value === "toUser"
? !formik.isValid || !formik.values.users.length
: !formik.isValid || !formik.values.roles.length)
}
onClick={() => {
if (!isNaN(id)) {
const formData = new FormData();
formData.append("message", formik.values.text);
formData.append("sender", isAdmin() ? "user" : "admin");
formData.append("send_message", false);
formData.append("ticket", id);
if (formik.values.image) {
formData.append("image", formik.values.image);
}
if (formik.values.uploadedFile) {
formData.append("file", formik.values.uploadedFile);
}
dispatch(sendResponseTicketService(formData)).then((r) => {
if (r.payload.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: r.payload.data.result,
severity: "error",
});
} else {
formik.resetForm();
setProfileImages([]);
fetchMessages();
getMessages();
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
}
});
} else {
if (value === "toUser") {
dispatch(
CreateTicketService({
type_ticket:
formik.values.users?.length === 1 ? "single" : "public",
to_user: formik.values.users.map((item) => {
return item?.key;
}),
image: formik.values.image ? formik.values.image : null,
title: formik.values.title,
sender: isAdmin() ? "user" : "admin",
message: formik.values.text,
read_only:
formik.values.users?.length === 1 ? false : isChecked,
role: selectedRole,
})
).then((r) => {
if (r.payload.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: r.payload.data.result,
severity: "error",
});
} else {
formik.resetForm();
navigate(-1);
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
}
});
} else {
dispatch(
CreateTicketService({
type_ticket: "public",
to_role: formik.values.roles,
image: formik.values.image ? formik.values.image : null,
title: formik.values.title,
sender: isAdmin() ? "user" : "admin",
message: formik.values.text,
read_only: isChecked,
role: selectedRole,
})
).then((r) => {
if (r.payload.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: r.payload.error,
severity: "error",
});
} else {
formik.resetForm();
navigate(-1);
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
}
});
}
}
}}
color="primary"
variant="contained"
fullWidth
type="submit"
style={{ marginTop: "16px" }}
>
ارسال
</Button>
</Grid>
);
};

View File

@@ -0,0 +1,168 @@
import React from "react";
import { Button, Divider, Typography } from "@mui/material";
import Tooltip, { tooltipClasses } from "@mui/material/Tooltip";
import { motion } from "framer-motion";
import DoneAllIcon from "@mui/icons-material/DoneAll";
import CheckIcon from "@mui/icons-material/Check";
import ShowImage from "../../../../components/show-image/ShowImage";
import persianDate from "persian-date";
import { Grid } from "../../../../components/grid/Grid";
import DownloadIcon from "@mui/icons-material/Download";
import { styled } from "@mui/material/styles";
const containerVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
};
const HtmlTooltip = styled(({ className, ...props }) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "#f5f5f9",
color: "rgba(0, 0, 0, 0.87)",
maxWidth: 220,
fontSize: theme.typography.pxToRem(12),
border: "1px solid #dadde9",
},
}));
const formatMessage = (message) => {
if (!message) return "";
const formattedText = message.replace(/\*\*\*/g, "\n").trim();
const lines = formattedText.split("\n");
return lines.map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
));
};
export const ShowMissages = ({ data }) => {
const isReffered = (item) => {
if (
item?.message?.includes("ارجاع داده شد.") &&
item?.message?.includes("تیکت شماره")
) {
return true;
} else {
return false;
}
};
return (
<Grid container gap={2}>
{data?.map((item, i) => (
<Grid
item
xs={12}
key={i}
component={motion.div}
variants={containerVariants}
initial="hidden"
animate="visible"
sx={{
border: "1px ridge gray",
borderRadius: "10px",
p: 2,
backgroundColor: isReffered(item) ? "#e7b2b2" : "background.paper",
boxShadow: 4,
}}
>
<Grid container justifyContent="space-between" alignItems="center">
<Typography color="text.secondary">
{item?.createdBy?.fullname}
</Typography>
<Grid
container
alignItems="center"
spacing={1}
justifyContent="flex-start"
>
<Typography color="text.secondary">
{`${new persianDate(new Date(item?.createdAt)).format(
"dddd DD MMMM"
)} (${new Date(item?.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})})`}
</Typography>
{item?.lastSeen ? (
<HtmlTooltip
disableHoverListener={!item?.readBy}
title={
<Grid container xs={12} direction="column">
<Typography variant="body1" color="primary">
بازدید شده توسط
</Typography>
{item?.readBy?.map((item, i) => (
<Typography variant="body2" key={i}>
{item?.fullname} ({item?.mobile})
</Typography>
))}
</Grid>
}
>
<DoneAllIcon sx={{ marginLeft: "10px" }} color="primary" />
</HtmlTooltip>
) : (
<CheckIcon sx={{ marginLeft: "10px" }} color="error" />
)}
</Grid>
</Grid>
<Divider sx={{ my: 1 }} />
<Typography
color={isReffered(item) ? "#202077" : "black"}
mt={1}
sx={{
textAlign: "left",
width: "100%",
}}
>
{formatMessage(item?.message)}
</Typography>
{(item?.picture || item?.file) && (
<>
<Divider sx={{ width: "100%", my: 2 }} />
<Grid
container
spacing={2}
mt={2}
justifyContent="space-between"
alignItems="center"
gap={2}
>
{item?.picture && (
<ShowImage src={item?.picture} size="100px" />
)}
{item?.file && (
<Button
color="success"
onClick={() => {
const link = item?.file;
window.location.href = link;
}}
endIcon={<DownloadIcon />}
>
دانلود فایل پیوست
</Button>
)}
</Grid>
</>
)}
</Grid>
))}
</Grid>
);
};

View File

@@ -0,0 +1,170 @@
import React, { useContext, useEffect, useState } from "react";
import useUserProfile from "../../../authentication/hooks/useUserProfile";
import { Grid } from "../../../../components/grid/Grid";
import {
Autocomplete,
Button,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
} from "@mui/material";
import { getFaUserRole } from "../../../../utils/getFaUserRole";
import { Yup } from "../../../../lib/yup/yup";
import { useFormik } from "formik";
import {
getTicketPermission,
getTicketUsersFromRole,
} from "../../services/get-ticket-permission";
import { useDispatch } from "react-redux";
import { EditTicketService } from "../../services/create-ticket";
import { AppContext } from "../../../../contexts/AppContext";
import { CLOSE_MODAL } from "../../../../lib/redux/slices/appSlice";
import { sortRoles } from "../../../../utils/sortRoles";
export const SubmitRefferTicket = ({ fetchMessages, ticket }) => {
const [role] = useUserProfile();
const [openNotif] = useContext(AppContext);
const [users, setUsers] = useState([]);
const dispatch = useDispatch();
const formik = useFormik({
initialValues: {
users: "",
roles: [],
},
validationSchema: Yup.object({
users: Yup.array().required("حداقل یک کاربر انتخاب کنید!"),
}),
onSubmit: (values) => {
// console.log(values);
},
});
const [permissionList, setPermissionList] = useState([]);
useEffect(() => {
dispatch(getTicketPermission({ role: sortRoles(role)[0] })).then((r) => {
setPermissionList(r.payload.data);
});
}, []);
useEffect(() => {
if (formik.values.roles.length) {
dispatch(getTicketUsersFromRole({ role: formik.values.roles })).then(
(r) => {
setUsers(r.payload.data);
}
);
}
}, [formik.values.roles]);
useEffect(() => {
formik.validateForm();
}, [dispatch]);
return (
<Grid
container
xs={12}
justifyContent="center"
alignItems="center"
gap={2}
direction="column"
>
<Grid item xs={12}>
{permissionList?.roles?.length ? (
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="role-select-label">انتخاب نقش</InputLabel>
<Select
labelId="role-select-label"
value={formik.values.roles}
onChange={(event) => {
formik.setFieldValue("roles", event.target.value);
}}
fullWidth
>
{permissionList?.roles.map((roleItem, i) => (
<MenuItem key={i} value={roleItem}>
{getFaUserRole(roleItem)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
) : (
<Typography variant="body1" color="error">
نقش انتخابی اجازه ارسال تیکت ندارد!
</Typography>
)}
</Grid>
<Grid xs={12}>
{users?.length ? (
<Grid item xs={12}>
<Autocomplete
multiple
id="tags-standard"
options={users}
getOptionLabel={(option) =>
`${option.fullname || "-"} (${option.mobile})`
}
onChange={(event, value) => {
formik.setFieldValue("users", value);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="انتخاب کاربر"
/>
)}
/>
</Grid>
) : (
<Typography color="error" variant="body2">
موردی یافت نشد!
</Typography>
)}
</Grid>
<Button
disabled={!formik.isValid}
fullWidth
variant="contained"
onClick={() => {
dispatch(
EditTicketService({
ticket: ticket,
referred_to: formik.values.users.map((item) => {
return item?.key;
}),
})
).then((r) => {
if (r.payload.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: r.payload.error,
severity: "error",
});
} else {
fetchMessages();
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
dispatch(CLOSE_MODAL());
}
});
}}
>
ارجاع
</Button>
</Grid>
);
};

View File

@@ -0,0 +1,28 @@
import React, { useState } from "react";
import { Grid } from "../../../../components/grid/Grid";
import { Tab, Tabs } from "@mui/material";
import { getRoleFromUrl } from "../../../../utils/getRoleFromUrl";
export const TicketAllView = () => {
const [value, setValue] = useState("0");
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Grid container xs={12} justifyContent="center" alignItems="center">
{(getRoleFromUrl() === "AdminX" ||
getRoleFromUrl() === "SuperAdmin" ||
getRoleFromUrl() === "ProvinceOperator") && (
<Tabs
value={value}
onChange={handleChange}
aria-label="secondary tabs example"
>
<Tab value="0" label="ارسال تکی" />
<Tab value="1" label="ارسال یکجا" />
</Tabs>
)}
</Grid>
);
};

View File

@@ -0,0 +1,166 @@
import {
Button,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
} from "@mui/material";
import { useFormik } from "formik";
import { useContext, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { Grid } from "../../../../components/grid/Grid";
import { ImageUpload } from "../../../../components/image-upload/ImageUpload";
import { AppContext } from "../../../../contexts/AppContext";
import { SPACING } from "../../../../data/spacing";
import {
DRAWER,
LOADING_END,
LOADING_START,
} from "../../../../lib/redux/slices/appSlice";
import { Yup } from "../../../../lib/yup/yup";
import { fixBase64 } from "../../../../utils/toBase64";
import { ticketCreateTicket } from "../../services/ticket-create-ticket";
import { ticketGetCreatedTickets } from "../../services/ticket-get-created-tickets";
export const TicketCreate = () => {
const dispatch = useDispatch();
const [openNotif] = useContext(AppContext);
const formik = useFormik({
initialValues: {
title: "",
supportUnit: "",
content: "",
image: [],
},
validationSchema: Yup.object({
title: Yup.string().required("این فیلد اجباری است!"),
supportUnit: Yup.string().required("این فیلد اجباری است!"),
content: Yup.string().required("این فیلد اجباری است!"),
}),
});
useEffect(() => {
formik.validateForm();
}, []);
const [attachmentImages, setAttachmentImages] = useState([]);
const attachmentsHandler = (imageList, addUpdateIndex) => {
const base64Urls = imageList.map((image) => fixBase64(image.data_url));
formik.setFieldValue("image", base64Urls);
setAttachmentImages(imageList);
};
return (
<Grid container justifyContent="space-between">
<Grid container flexGrow="1" gap={SPACING.SMALL} direction="column">
<Grid>
<TextField
fullWidth
id="title"
label="موضوع"
value={formik.values.title}
error={formik.touched.title ? Boolean(formik.errors.title) : null}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
helperText={
formik.touched.title && Boolean(formik.errors.title)
? formik.errors.title
: null
}
variant="outlined"
/>
</Grid>
<Grid>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">واحد مربوطه</InputLabel>
<Select
fullWidth
value={formik.values.supportUnit}
id="supportUnit"
label="واحد مربوطه"
onChange={(e) => {
formik.setFieldValue("supportUnit", e.target.value);
}}
>
<MenuItem value={"CityOperator"}>شهرستان</MenuItem>
<MenuItem value={"ProvinceOperator"}>تخصیص استان</MenuItem>
<MenuItem value={"ProvinceInspector"}>بازرسی استان</MenuItem>
<MenuItem value={"ProvinceFinancial"}>واحد مالی استان</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid>
<TextField
id="content"
label="توضیحات"
multiline
rows={5}
variant="outlined"
sx={{ width: "100%", height: "100%" }}
value={formik.values.content}
error={
formik.touched.content ? Boolean(formik.errors.content) : null
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
helperText={
formik.touched.content && Boolean(formik.errors.content)
? formik.errors.content
: null
}
/>
</Grid>
<Grid>
<ImageUpload
onChange={attachmentsHandler}
images={attachmentImages}
maxNumber={3}
title={"افزودن پیوست"}
/>
</Grid>
<Button
fullWidth
variant="contained"
disabled={!formik.isValid}
onClick={() => {
dispatch(LOADING_START());
dispatch(
ticketCreateTicket({
title: formik.values.title,
content: formik.values.content,
support_unit: formik.values.supportUnit,
image: formik.values.image,
})
).then((r) => {
dispatch(LOADING_END());
if (r.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: "مشکلی پیش آمده است!",
severity: "error",
});
} else {
dispatch(ticketGetCreatedTickets());
dispatch(
DRAWER({ right: false, bottom: false, content: null })
);
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
}
});
}}
>
ارسال تیکت
</Button>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,79 @@
import {
TimelineConnector,
TimelineContent,
TimelineDot,
TimelineItem,
TimelineSeparator,
} from "@mui/lab";
import { Typography } from "@mui/material";
import { PropTypes } from "prop-types";
import { Grid } from "../../../../components/grid/Grid";
import { SPACING } from "../../../../data/spacing";
import { formatTime } from "../../../../utils/formatTime";
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
export const TicketCustomerItem = ({ item }) => {
return (
<TimelineItem>
<TimelineSeparator>
<TimelineDot color="warning">
<QuestionMarkIcon fontSize="small" />
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<Grid container direction="column" mt={1}>
<TimelineContent>
<Grid container gap={SPACING.TINY}>
<Typography variant="body1" fontWeight="bold">
{item.title}
</Typography>
<Typography variant="body1">-</Typography>
<Typography
variant="body1"
color={(prop) => prop.palette.grey["A700"]}
>
{formatTime(item.createDate)}
</Typography>
</Grid>
</TimelineContent>
<TimelineContent>
<Grid container direction="column" gap={SPACING.SMALL}>
<Grid>
<Typography varinat="body2">{item.content}</Typography>
</Grid>
{Boolean(item.image?.length) && (
<>
<Grid>
<Typography
varinat="body1"
color={(prop) => prop.palette.grey["A700"]}
>
پیوست ها
</Typography>
</Grid>
<Grid container gap={SPACING.SMALL}>
{item.image.map((img, i) => {
return (
<a key={"ticket-img" + i} href={img}>
<img
src={img}
width="100"
alt="ticket"
style={{ borderRadius: "10px" }}
/>
</a>
);
})}
</Grid>
</>
)}
</Grid>
</TimelineContent>
</Grid>
</TimelineItem>
);
};
TicketCustomerItem.propTypes = {
item: PropTypes.any,
};

View File

@@ -0,0 +1,79 @@
import {
TimelineConnector,
TimelineContent,
TimelineDot,
TimelineItem,
TimelineSeparator,
} from "@mui/lab";
import { Typography } from "@mui/material";
import { PropTypes } from "prop-types";
import { Grid } from "../../../../components/grid/Grid";
import { SPACING } from "../../../../data/spacing";
import { formatTime } from "../../../../utils/formatTime";
import SupportAgentIcon from "@mui/icons-material/SupportAgent";
export const TicketOperatorItem = ({ item }) => {
return (
<TimelineItem>
<TimelineSeparator>
<TimelineDot color="primary">
<SupportAgentIcon fontSize="small" />
</TimelineDot>
<TimelineConnector />
</TimelineSeparator>
<Grid container direction="column" mt={1}>
<TimelineContent>
<Grid container gap={SPACING.TINY}>
<Typography variant="body1" fontWeight="bold">
{item.title}
</Typography>
<Typography variant="body1">-</Typography>
<Typography
variant="body1"
color={(prop) => prop.palette.grey["A700"]}
>
{formatTime(item.createDate)}
</Typography>
</Grid>
</TimelineContent>
<TimelineContent>
<Grid container direction="column" gap={SPACING.SMALL}>
<Grid>
<Typography varinat="body2">{item.content}</Typography>
</Grid>
{Boolean(item.image?.length) && (
<>
<Grid>
<Typography
varinat="body1"
color={(prop) => prop.palette.grey["A700"]}
>
پیوست ها
</Typography>
</Grid>
<Grid container gap={SPACING.SMALL}>
{item.image.map((img, i) => {
return (
<a key={"ticket-img" + i} href={img}>
<img
src={img}
width="100"
alt="ticket"
style={{ borderRadius: "10px" }}
/>
</a>
);
})}
</Grid>
</>
)}
</Grid>
</TimelineContent>
</Grid>
</TimelineItem>
);
};
TicketOperatorItem.propTypes = {
item: PropTypes.any,
};

View File

@@ -0,0 +1,157 @@
import { Button, TextField } from "@mui/material";
import { useFormik } from "formik";
import { Grid } from "../../../../components/grid/Grid";
import { Yup } from "../../../../lib/yup/yup";
import { PropTypes } from "prop-types";
import { SPACING } from "../../../../data/spacing";
import { useContext, useEffect, useState } from "react";
import { fixBase64 } from "../../../../utils/toBase64";
import {
DRAWER,
LOADING_END,
LOADING_START,
} from "../../../../lib/redux/slices/appSlice";
import { ticketRespondTicket } from "../../services/ticket-respond-ticket";
import { useDispatch } from "react-redux";
import { AppContext } from "../../../../contexts/AppContext";
import { ImageUpload } from "../../../../components/image-upload/ImageUpload";
import { ticketCreateTicket } from "../../services/ticket-create-ticket";
import { ticketGetCreatedTickets } from "../../services/ticket-get-created-tickets";
import { ticketGetOperatorTickets } from "../../services/ticket-get-operator-tickets";
export const TicketRespond = ({
ticketKey,
questionKey,
customer,
supportUnit,
}) => {
const [openNotif] = useContext(AppContext);
const dispatch = useDispatch();
const formik = useFormik({
initialValues: {
content: "",
title: "",
image: [],
},
validationSchema: Yup.object({
content: Yup.string().required("این فیلد اجباری است!"),
title: Yup.string().required("این فیلد اجباری است!"),
}),
});
useEffect(() => {
formik.validateForm();
}, []);
const [attachmentImages, setAttachmentImages] = useState([]);
const attachmentsHandler = (imageList, addUpdateIndex) => {
const base64Urls = imageList.map((image) => fixBase64(image.data_url));
formik.setFieldValue("image", base64Urls);
setAttachmentImages(imageList);
};
return (
<Grid container direction="column" gap={SPACING.TINY}>
<Grid>
<TextField
id="title"
label="عنوان پاسخ"
variant="outlined"
value={formik.values.title}
error={formik.touched.title ? Boolean(formik.errors.title) : null}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
helperText={
formik.touched.title && Boolean(formik.errors.title)
? formik.errors.title
: null
}
/>
</Grid>
<Grid>
<TextField
multiline
rows={4}
fullWidth
id="content"
label="توضیحات"
variant="outlined"
value={formik.values.content}
error={formik.touched.content ? Boolean(formik.errors.content) : null}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
helperText={
formik.touched.content && Boolean(formik.errors.content)
? formik.errors.content
: null
}
/>
</Grid>
<Grid>
<ImageUpload
onChange={attachmentsHandler}
images={attachmentImages}
maxNumber={3}
title={"افزودن پیوست"}
/>
</Grid>
<Grid>
<Button
fullWidth
variant="contained"
disabled={!formik.isValid}
onClick={() => {
dispatch(LOADING_START());
const respondType = customer
? ticketCreateTicket({
ticket_key: ticketKey,
support_unit: supportUnit,
title: formik.values.title,
content: formik.values.content,
image: formik.values.image,
})
: ticketRespondTicket({
title: formik.values.title,
content: formik.values.content,
ticket_key: ticketKey,
// question_key: questionKey,
image: formik.values.image,
});
dispatch(respondType).then((r) => {
dispatch(LOADING_END());
if (r.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: "مشکلی پیش آمده است!",
severity: "error",
});
} else {
dispatch(ticketGetCreatedTickets());
dispatch(ticketGetOperatorTickets());
dispatch(
DRAWER({ right: false, bottom: false, content: null })
);
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
}
});
}}
>
ثبت
</Button>
</Grid>
</Grid>
);
};
TicketRespond.propTypes = {
ticketKey: PropTypes.any,
questionKey: PropTypes.any,
customer: PropTypes.any,
supportUnit: PropTypes.any,
};

View File

@@ -0,0 +1,187 @@
import React, { useEffect, useState } from "react";
import { Grid } from "../../../../components/grid/Grid";
import { BackButton } from "../../../../components/back-button/BackButton";
import { useParams } from "react-router-dom";
import { CreateTicket } from "../create-ticket/CreateTicket";
import { ShowMissages } from "../show-messages/ShowMissages";
import { useDispatch } from "react-redux";
import { getMessagesService } from "../../services/get-messages";
import { Button, Typography } from "@mui/material";
import { formatJustDate } from "../../../../utils/formatTime";
import ForwardToInboxIcon from "@mui/icons-material/ForwardToInbox";
import { OPEN_MODAL } from "../../../../lib/redux/slices/appSlice";
import { SubmitRefferTicket } from "../submit-reffer-ticket/SubmitRefferTicket";
export const TicketView = () => {
const { create, id } = useParams();
const dispatch = useDispatch();
const [data, setData] = useState();
const [canSeeMessages, setCanSeeMessages] = useState();
const fetchMessages = () => {
dispatch(getMessagesService({ ticket: id })).then((r) => {
setData(r.payload.data);
});
};
useEffect(() => {
let intervalId;
if (create === "false") {
fetchMessages();
intervalId = setInterval(fetchMessages, 3000);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [dispatch, id, create]);
useEffect(() => {
if (data?.length) {
if (
data[0]?.ticket?.readOnly === true ||
data[0]?.ticket?.status === "closed"
) {
setCanSeeMessages(false);
}
} else {
setCanSeeMessages(true);
}
}, [data]);
const isReffered = (data) => {
if (data) {
if (
data.some(
(item) =>
item.message?.includes("ارجاع داده شد.") &&
item?.message?.includes("تیکت شماره")
)
) {
return true;
} else {
return false;
}
}
};
return (
<Grid container xs={12} justifyContent="center">
<Grid container xs={12}>
<BackButton />
</Grid>
{data && (
<Grid
container
xs={12}
justifyContent="space-between"
alignItems="center"
p={2}
mb={2}
gap={2}
direction={{ xs: "column", sm: "row" }}
sx={{
borderStyle: "solid",
borderWidth: "1px",
borderRadius: "30px",
backgroundColor: "#e5e5e5",
}}
>
<Grid container alignItems="center">
<Typography>عنوان: {""}</Typography>
<Typography>{data[0]?.ticket?.title}</Typography>
</Grid>
<Grid container alignItems="center">
<Typography>تاریخ ایجاد: {""}</Typography>
<Typography>
{formatJustDate(data[0]?.ticket?.createDate)}
</Typography>
</Grid>
<Grid container alignItems="center">
<Typography>وضعیت تیکت: {""}</Typography>
<Typography>
{data[0]?.ticket?.status === "open"
? "باز"
: data[0]?.ticket?.status === "answered"
? "باز"
: "بسته"}
{isReffered(data) && " (ارجاع داده شده) "}
{data[0]?.ticket?.readOnly && "(فقط خواندنی)"}
</Typography>
</Grid>
{Object.prototype.hasOwnProperty.call(data[0], "readBy") && (
<Grid container alignItems="center">
<Button
variant="outlined"
endIcon={<ForwardToInboxIcon />}
onClick={() => {
dispatch(
OPEN_MODAL({
title: "ارجاع تیکت",
content: (
<SubmitRefferTicket
fetchMessages={fetchMessages}
ticket={id}
/>
),
})
);
}}
>
ارجاع
</Button>
</Grid>
)}
{/* <Grid container alignItems="center">
<Typography>دریافت کنندگان: {""}</Typography>
<Typography>
{data[0]?.ticket?.toUser.length
? data[0]?.ticket?.toUser?.map(
(option, index) =>
`${option?.fullname} ${
index + 1 !== data[0]?.ticket?.toUser?.length
? " - "
: ""
}`
)
: data[0]?.ticket?.toRole?.map(
(option, index) =>
`${getFaUserRole(option.name)} ${
index + 1 !== data[0]?.ticket?.toRole?.length
? " - "
: ""
}`
)}
</Typography>
</Grid> */}
</Grid>
)}
{canSeeMessages && (
<Grid
xs={12}
md={4}
justifyContent="center"
alignItems="center"
sx={{ paddingRight: "17px" }}
>
<CreateTicket id={id} fetchMessages={fetchMessages} />
</Grid>
)}
{create === "false" && (
<Grid
xs={12}
md={8}
sx={{ marginTop: { xs: 2, md: 0 } }}
justifyContent="center"
alignItems="center"
>
<ShowMissages id={id} data={data} />
</Grid>
)}
</Grid>
);
};

View File

@@ -0,0 +1,44 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
import { LOADING_END, LOADING_START } from "../../../lib/redux/slices/appSlice";
export const CreateTicketService = createAsyncThunk(
"CREATE_TICKET",
async (d, { dispatch }) => {
dispatch(LOADING_START());
try {
const { data, status } = await axios.post("ticket/", d);
dispatch(LOADING_END());
return { data, status };
} catch (e) {
dispatch(LOADING_END());
return { error: e.response.data.result };
}
}
);
export const CloseTicketService = createAsyncThunk(
"CLOSE_TICKET",
async (d, { dispatch }) => {
dispatch(LOADING_START());
const { data, status } = await axios.put("ticket/0/", d);
dispatch(LOADING_END());
return { data, status };
}
);
export const EditTicketService = createAsyncThunk(
"EDIT_TICKET",
async (d, { dispatch }) => {
dispatch(LOADING_START());
try {
const { data, status } = await axios.put("ticket/0/", d);
dispatch(LOADING_END());
return { data, status };
} catch (e) {
dispatch(LOADING_END());
return { error: e.response.data.result };
}
}
);

View File

@@ -0,0 +1,14 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const getMessagesService = createAsyncThunk(
"GET_MESSAGES",
async (d, { dispatch }) => {
const { data, status } = await axios.get(`message/`, {
params: {
ticket: d.ticket,
},
});
return { data, status };
}
);

View File

@@ -0,0 +1,34 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
import { LOADING_END, LOADING_START } from "../../../lib/redux/slices/appSlice";
export const getTicketPermission = createAsyncThunk(
"GET_TICKET_PERMISSION",
async (d, { dispatch }) => {
dispatch(LOADING_START());
const { data, status } = await axios.get(`ticket-permission/`, {
params: {
role: d.role,
},
});
dispatch(LOADING_END());
return { data, status };
}
);
export const getTicketUsersFromRole = createAsyncThunk(
"GET_TICKET_USERS_FROM_ROLE",
async (d, { dispatch }) => {
dispatch(LOADING_START());
const roles = Array.isArray(d.role) ? d.role.join(",") : d.role;
const { data, status } = await axios.get(`get-user-from-role/`, {
params: {
role: roles,
},
});
dispatch(LOADING_END());
return { data, status };
}
);

View File

@@ -0,0 +1,13 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
import { LOADING_END, LOADING_START } from "../../../lib/redux/slices/appSlice";
export const sendResponseTicketService = createAsyncThunk(
"RESPONSE_TICKET",
async (d, { dispatch }) => {
dispatch(LOADING_START());
const { data, status } = await axios.post("message/", d);
dispatch(LOADING_END());
return { data, status };
}
);

View File

@@ -0,0 +1,10 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const ticketCloseTicket = createAsyncThunk(
"TICKET_CLOSE_TICKET",
async (d) => {
const { data, status } = await axios.put("/create_ticket/0/", d);
return { data, status };
}
);

View File

@@ -0,0 +1,10 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const ticketCreateTicket = createAsyncThunk(
"TICKET_CREATE_TICKET",
async (d) => {
const { data, status } = await axios.post("/create_ticket/", d);
return { data, status };
}
);

View File

@@ -0,0 +1,12 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const ticketGetCreatedTickets = createAsyncThunk(
"TICKET_GET_CREATED_TICKETS",
async () => {
const { data, status } = await axios.get("/create_ticket/", {
params: { all: true },
});
return { data, status };
}
);

View File

@@ -0,0 +1,12 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const ticketGetOperatorTickets = createAsyncThunk(
"TICKET_GET_OPERATOR_TICKETS",
async () => {
const { data, status } = await axios.get("/respond/", {
params: { all: true },
});
return { data, status };
}
);

View File

@@ -0,0 +1,10 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const ticketRespondTicket = createAsyncThunk(
"TICKET_RESPOND_TICKET",
async (d) => {
const { data, status } = await axios.post("/respond/", d);
return { data, status };
}
);