Merge branch 'master' into _dialogflow_omnihit_hit
commit
280c006d02
|
@ -7,9 +7,9 @@ import UpdateSettingService from "../services/SettingServices/UpdateSettingServi
|
|||
import ListSettingsService from "../services/SettingServices/ListSettingsService";
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (req.user.profile !== "master") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
// if (req.user.profile !== "master") {
|
||||
// throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
// }
|
||||
|
||||
const settings = await ListSettingsService();
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkInsert(
|
||||
"Settings",
|
||||
[
|
||||
{
|
||||
key: "editURA",
|
||||
value: "enabled",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
],
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkDelete("Settings", {});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkInsert(
|
||||
"Settings",
|
||||
[
|
||||
{
|
||||
key: "editQueue",
|
||||
value: "enabled",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
],
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkDelete("Settings", {});
|
||||
}
|
||||
};
|
|
@ -5,9 +5,7 @@ import * as SettingController from "../controllers/SettingController";
|
|||
|
||||
const settingRoutes = Router();
|
||||
|
||||
|
||||
|
||||
settingRoutes.get("/settings", isAuth, SettingController.index);
|
||||
settingRoutes.get("/settings", SettingController.index);
|
||||
|
||||
// routes.get("/settings/:settingKey", isAuth, SettingsController.show);
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import StatusChatEnd from "../../models/StatusChatEnd";
|
||||
import AppError from "../../errors/AppError";
|
||||
|
||||
const ShowStatusChatEndService = async (id: string | number): Promise<StatusChatEnd> => {
|
||||
const status = await StatusChatEnd.findByPk(id, { attributes: ['id', 'name'], });
|
||||
const ShowStatusChatEndService = async (
|
||||
id: string | number
|
||||
): Promise<StatusChatEnd> => {
|
||||
const status = await StatusChatEnd.findByPk(id, {
|
||||
attributes: ["id", "name"]
|
||||
});
|
||||
|
||||
console.log(`---------------> statusChatEnd id: ${id}`);
|
||||
|
||||
if (!status) {
|
||||
throw new AppError("ERR_NO_STATUS_FOUND", 404);
|
||||
|
|
|
@ -4,21 +4,18 @@ import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp";
|
|||
import Ticket from "../../models/Ticket";
|
||||
import ShowContactService from "../ContactServices/ShowContactService";
|
||||
|
||||
|
||||
import { getIO } from "../../libs/socket";
|
||||
import ShowUserServiceReport from "../UserServices/ShowUserServiceReport";
|
||||
|
||||
import format from 'date-fns/format';
|
||||
import ptBR from 'date-fns/locale/pt-BR';
|
||||
import format from "date-fns/format";
|
||||
import ptBR from "date-fns/locale/pt-BR";
|
||||
import { splitDateTime } from "../../helpers/SplitDateTime";
|
||||
import TicketEmiterSumOpenClosedByUser from "../../helpers/OnlineReporEmiterInfoByUser";
|
||||
|
||||
import { createOrUpdateTicketCache } from '../../helpers/TicketCache'
|
||||
import { createOrUpdateTicketCache } from "../../helpers/TicketCache";
|
||||
import User from "../../models/User";
|
||||
import whatsappQueueMatchingUserQueue from "../../helpers/whatsappQueueMatchingUserQueue";
|
||||
let flatten = require('flat')
|
||||
|
||||
|
||||
let flatten = require("flat");
|
||||
|
||||
interface Request {
|
||||
contactId: number;
|
||||
|
@ -33,17 +30,19 @@ const CreateTicketService = async ({
|
|||
userId,
|
||||
queueId = undefined
|
||||
}: Request): Promise<Ticket> => {
|
||||
|
||||
console.log("========> queueId: ", queueId);
|
||||
|
||||
try {
|
||||
|
||||
const defaultWhatsapp = await GetDefaultWhatsApp(userId);
|
||||
|
||||
|
||||
if (!queueId) {
|
||||
const user = await User.findByPk(userId, { raw: true, })
|
||||
const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp, user?.profile);
|
||||
queueId = matchingQueue ? matchingQueue.queueId : undefined
|
||||
const user = await User.findByPk(userId, { raw: true });
|
||||
const matchingQueue = await whatsappQueueMatchingUserQueue(
|
||||
userId,
|
||||
defaultWhatsapp,
|
||||
user?.profile
|
||||
);
|
||||
queueId = matchingQueue ? matchingQueue.queueId : undefined;
|
||||
}
|
||||
|
||||
await CheckContactOpenTickets(contactId);
|
||||
|
@ -66,27 +65,32 @@ const CreateTicketService = async ({
|
|||
|
||||
// console.log('CONTACT ticket.id: ', ticket.id)
|
||||
|
||||
|
||||
// TEST DEL
|
||||
try {
|
||||
|
||||
let jsonString = JSON.stringify(ticket); //convert to string to remove the sequelize specific meta data
|
||||
let ticket_obj = JSON.parse(jsonString); //to make plain json
|
||||
delete ticket_obj['contact']['extraInfo']
|
||||
delete ticket_obj["contact"]["extraInfo"];
|
||||
|
||||
ticket_obj = flatten(ticket_obj)
|
||||
|
||||
await createOrUpdateTicketCache(`ticket:${ticket.id}`, ticket_obj)
|
||||
ticket_obj = flatten(ticket_obj);
|
||||
|
||||
await createOrUpdateTicketCache(`ticket:${ticket.id}`, ticket_obj);
|
||||
} catch (error) {
|
||||
console.log('There was an error on UpdateTicketService.ts on createTicketCache from user: ', error)
|
||||
console.log(
|
||||
"There was an error on UpdateTicketService.ts on createTicketCache from user: ",
|
||||
error
|
||||
);
|
||||
}
|
||||
//
|
||||
|
||||
const dateToday = splitDateTime(
|
||||
new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR }))
|
||||
);
|
||||
|
||||
const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
|
||||
|
||||
TicketEmiterSumOpenClosedByUser(userId.toString(), dateToday.fullDate, dateToday.fullDate)
|
||||
TicketEmiterSumOpenClosedByUser(
|
||||
userId.toString(),
|
||||
dateToday.fullDate,
|
||||
dateToday.fullDate
|
||||
);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("ticketStatus", {
|
||||
|
@ -94,14 +98,11 @@ const CreateTicketService = async ({
|
|||
ticketStatus: { ticketId: ticket.id, status: ticket.status }
|
||||
});
|
||||
|
||||
|
||||
return ticket;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('===> Error on CreateTicketService.ts file: \n', error)
|
||||
console.error("===> Error on CreateTicketService.ts file: \n", error);
|
||||
throw new AppError(error.message);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default CreateTicketService;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,57 +1,54 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import Routes from "./routes";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Routes from './routes'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
|
||||
import { ptBR } from "@material-ui/core/locale";
|
||||
import { createTheme, ThemeProvider } from '@material-ui/core/styles'
|
||||
import { ptBR } from '@material-ui/core/locale'
|
||||
|
||||
import { TabTicketProvider } from "../src/context/TabTicketHeaderOption/TabTicketHeaderOption";
|
||||
import { TabTicketProvider } from '../src/context/TabTicketHeaderOption/TabTicketHeaderOption'
|
||||
|
||||
const App = () => {
|
||||
const [locale, setLocale] = useState();
|
||||
const [locale, setLocale] = useState()
|
||||
|
||||
const theme = createTheme(
|
||||
{
|
||||
scrollbarStyles: {
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
boxShadow: 'inset 0 0 6px rgba(0, 0, 0, 0.3)',
|
||||
|
||||
backgroundColor: "#e8e8e8",
|
||||
backgroundColor: '#e8e8e8',
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
//primary: { main: "#2576d2" },
|
||||
primary: { main: "#ec5114" },
|
||||
primary: { main: '#ec5114' },
|
||||
},
|
||||
},
|
||||
locale
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const i18nlocale = localStorage.getItem("i18nextLng");
|
||||
const i18nlocale = localStorage.getItem('i18nextLng')
|
||||
const browserLocale =
|
||||
i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5);
|
||||
i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5)
|
||||
|
||||
if (browserLocale === "ptBR") {
|
||||
setLocale(ptBR);
|
||||
if (browserLocale === 'ptBR') {
|
||||
setLocale(ptBR)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
|
||||
{/*TabTicketProvider Context to manipulate the entire state of selected option from user click on tickets options header */}
|
||||
<TabTicketProvider>
|
||||
<Routes />
|
||||
</TabTicketProvider>
|
||||
|
||||
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
|
|
@ -1,290 +1,300 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import { toast } from "react-toastify";
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { green } from '@material-ui/core/colors'
|
||||
|
||||
import { AuthContext } from '../../context/Auth/AuthContext'
|
||||
import { Can } from '../../components/Can'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Button,
|
||||
DialogActions,
|
||||
CircularProgress,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from "@material-ui/core";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Button,
|
||||
DialogActions,
|
||||
CircularProgress,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core'
|
||||
|
||||
import api from "../../services/api";
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import toastError from "../../errors/toastError";
|
||||
import QueueSelect from "../QueueSelect";
|
||||
import api from '../../services/api'
|
||||
import { i18n } from '../../translate/i18n'
|
||||
import toastError from '../../errors/toastError'
|
||||
import QueueSelect from '../QueueSelect'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
|
||||
multFieldLine: {
|
||||
display: "flex",
|
||||
"& > *:not(:last-child)": {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
multFieldLine: {
|
||||
display: 'flex',
|
||||
'& > *:not(:last-child)': {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
|
||||
btnWrapper: {
|
||||
position: "relative",
|
||||
},
|
||||
btnWrapper: {
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
buttonProgress: {
|
||||
color: green[500],
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
marginTop: -12,
|
||||
marginLeft: -12,
|
||||
},
|
||||
}));
|
||||
buttonProgress: {
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: -12,
|
||||
marginLeft: -12,
|
||||
},
|
||||
}))
|
||||
|
||||
const SessionSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.min(2, "Too Short!")
|
||||
.max(100, "Too Long!")
|
||||
.required("Required"),
|
||||
});
|
||||
name: Yup.string()
|
||||
.min(2, 'Too Short!')
|
||||
.max(100, 'Too Long!')
|
||||
.required('Required'),
|
||||
})
|
||||
|
||||
const WhatsAppModal = ({ open, onClose, whatsAppId }) => {
|
||||
const classes = useStyles();
|
||||
const initialState = {
|
||||
name: "",
|
||||
urlApi: "",
|
||||
url: "",
|
||||
greetingMessage: "",
|
||||
farewellMessage: "",
|
||||
isDefault: false,
|
||||
};
|
||||
const [whatsApp, setWhatsApp] = useState(initialState);
|
||||
const [selectedQueueIds, setSelectedQueueIds] = useState([]);
|
||||
const classes = useStyles()
|
||||
const initialState = {
|
||||
name: '',
|
||||
urlApi: '',
|
||||
url: '',
|
||||
greetingMessage: '',
|
||||
farewellMessage: '',
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
if (!whatsAppId) return;
|
||||
const { user } = useContext(AuthContext)
|
||||
|
||||
try {
|
||||
const { data } = await api.get(`whatsapp/${whatsAppId}`);
|
||||
setWhatsApp(data);
|
||||
const [whatsApp, setWhatsApp] = useState(initialState)
|
||||
const [selectedQueueIds, setSelectedQueueIds] = useState([])
|
||||
|
||||
const whatsQueueIds = data.queues?.map(queue => queue.id);
|
||||
setSelectedQueueIds(whatsQueueIds);
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
}
|
||||
};
|
||||
fetchSession();
|
||||
}, [whatsAppId]);
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
if (!whatsAppId) return
|
||||
|
||||
const handleSaveWhatsApp = async values => {
|
||||
const whatsappData = { ...values, queueIds: selectedQueueIds };
|
||||
try {
|
||||
const { data } = await api.get(`whatsapp/${whatsAppId}`)
|
||||
setWhatsApp(data)
|
||||
|
||||
let response = null
|
||||
const whatsQueueIds = data.queues?.map((queue) => queue.id)
|
||||
setSelectedQueueIds(whatsQueueIds)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
fetchSession()
|
||||
}, [whatsAppId])
|
||||
|
||||
try {
|
||||
if (whatsAppId) {
|
||||
response = await api.put(`/whatsapp/${whatsAppId}`, whatsappData);
|
||||
} else {
|
||||
response = await api.post("/whatsapp", whatsappData);
|
||||
}
|
||||
const handleSaveWhatsApp = async (values) => {
|
||||
const whatsappData = { ...values, queueIds: selectedQueueIds }
|
||||
|
||||
console.log('response: ', response.data.message)
|
||||
let response = null
|
||||
|
||||
if(response && response.data.message === 'wrong_number_start'){
|
||||
alert('O numero contido no nome da conexão deve iniciar com o código do país!')
|
||||
}
|
||||
else if(response && response.data.message === 'invalid_phone_number'){
|
||||
alert('A quantidade de numeros digitados no nome do contato é invalida! Certifique-se de que você digitou o numero correto acompanhado pelo código do país!')
|
||||
}
|
||||
else if( response && response.data.message === 'no_phone_number'){
|
||||
alert('Para criar/editar uma sessão de Whatsapp é necessário que o numero do Whatsapp acompanhado pelo código do país esteja presente no nome da sessão!')
|
||||
}
|
||||
else{
|
||||
toast.success(i18n.t("whatsappModal.success"));
|
||||
handleClose();
|
||||
}
|
||||
try {
|
||||
if (whatsAppId) {
|
||||
response = await api.put(`/whatsapp/${whatsAppId}`, whatsappData)
|
||||
} else {
|
||||
response = await api.post('/whatsapp', whatsappData)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
}
|
||||
};
|
||||
console.log('response: ', response.data.message)
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setWhatsApp(initialState);
|
||||
};
|
||||
if (response && response.data.message === 'wrong_number_start') {
|
||||
alert(
|
||||
'O numero contido no nome da conexão deve iniciar com o código do país!'
|
||||
)
|
||||
} else if (response && response.data.message === 'invalid_phone_number') {
|
||||
alert(
|
||||
'A quantidade de numeros digitados no nome do contato é invalida! Certifique-se de que você digitou o numero correto acompanhado pelo código do país!'
|
||||
)
|
||||
} else if (response && response.data.message === 'no_phone_number') {
|
||||
alert(
|
||||
'Para criar/editar uma sessão de Whatsapp é necessário que o numero do Whatsapp acompanhado pelo código do país esteja presente no nome da sessão!'
|
||||
)
|
||||
} else {
|
||||
toast.success(i18n.t('whatsappModal.success'))
|
||||
handleClose()
|
||||
}
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
scroll="paper"
|
||||
>
|
||||
<DialogTitle>
|
||||
{whatsAppId
|
||||
? i18n.t("whatsappModal.title.edit")
|
||||
: i18n.t("whatsappModal.title.add")}
|
||||
</DialogTitle>
|
||||
<Formik
|
||||
initialValues={whatsApp}
|
||||
enableReinitialize={true}
|
||||
validationSchema={SessionSchema}
|
||||
onSubmit={(values, actions) => {
|
||||
setTimeout(() => {
|
||||
handleSaveWhatsApp(values);
|
||||
actions.setSubmitting(false);
|
||||
}, 400);
|
||||
}}
|
||||
>
|
||||
{({ values, touched, errors, isSubmitting }) => (
|
||||
<Form>
|
||||
<DialogContent dividers>
|
||||
const handleClose = () => {
|
||||
onClose()
|
||||
setWhatsApp(initialState)
|
||||
}
|
||||
|
||||
<div className={classes.multFieldLine}>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t("whatsappModal.form.name")}
|
||||
autoFocus
|
||||
name="name"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Field
|
||||
as={Switch}
|
||||
color="primary"
|
||||
name="isDefault"
|
||||
checked={values.isDefault}
|
||||
/>
|
||||
}
|
||||
label={i18n.t("whatsappModal.form.default")}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
scroll="paper"
|
||||
>
|
||||
<DialogTitle>
|
||||
{whatsAppId
|
||||
? i18n.t('whatsappModal.title.edit')
|
||||
: i18n.t('whatsappModal.title.add')}
|
||||
</DialogTitle>
|
||||
<Formik
|
||||
initialValues={whatsApp}
|
||||
enableReinitialize={true}
|
||||
validationSchema={SessionSchema}
|
||||
onSubmit={(values, actions) => {
|
||||
setTimeout(() => {
|
||||
handleSaveWhatsApp(values)
|
||||
actions.setSubmitting(false)
|
||||
}, 400)
|
||||
}}
|
||||
>
|
||||
{({ values, touched, errors, isSubmitting }) => (
|
||||
<Form>
|
||||
<DialogContent dividers>
|
||||
<Can
|
||||
role={user.profile}
|
||||
perform="url-remote-session:show"
|
||||
yes={() => (
|
||||
<>
|
||||
<div className={classes.multFieldLine}>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t('whatsappModal.form.name')}
|
||||
autoFocus
|
||||
name="name"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Field
|
||||
as={Switch}
|
||||
color="primary"
|
||||
name="isDefault"
|
||||
checked={values.isDefault}
|
||||
/>
|
||||
}
|
||||
label={i18n.t('whatsappModal.form.default')}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.multFieldLine}>
|
||||
<Field
|
||||
as={TextField}
|
||||
label="url API"
|
||||
autoFocus
|
||||
name="urlApi"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
<Field
|
||||
as={TextField}
|
||||
label="url session"
|
||||
autoFocus
|
||||
name="url"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t('queueModal.form.greetingMessage')}
|
||||
type="greetingMessage"
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
name="greetingMessage"
|
||||
error={
|
||||
touched.greetingMessage && Boolean(errors.greetingMessage)
|
||||
}
|
||||
helperText={
|
||||
touched.greetingMessage && errors.greetingMessage
|
||||
}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t('whatsappModal.form.farewellMessage')}
|
||||
type="farewellMessage"
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
name="farewellMessage"
|
||||
error={
|
||||
touched.farewellMessage && Boolean(errors.farewellMessage)
|
||||
}
|
||||
helperText={
|
||||
touched.farewellMessage && errors.farewellMessage
|
||||
}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
/>
|
||||
</div>
|
||||
<QueueSelect
|
||||
selectedQueueIds={selectedQueueIds}
|
||||
onChange={(selectedIds) => setSelectedQueueIds(selectedIds)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="secondary"
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
>
|
||||
{i18n.t('whatsappModal.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
variant="contained"
|
||||
className={classes.btnWrapper}
|
||||
>
|
||||
{whatsAppId
|
||||
? i18n.t('whatsappModal.buttons.okEdit')
|
||||
: i18n.t('whatsappModal.buttons.okAdd')}
|
||||
{isSubmitting && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
className={classes.buttonProgress}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={classes.multFieldLine}>
|
||||
<Field
|
||||
as={TextField}
|
||||
label='url API'
|
||||
autoFocus
|
||||
name="urlApi"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
<Field
|
||||
as={TextField}
|
||||
label='url session'
|
||||
autoFocus
|
||||
name="url"
|
||||
error={touched.name && Boolean(errors.name)}
|
||||
helperText={touched.name && errors.name}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
className={classes.textField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t("queueModal.form.greetingMessage")}
|
||||
type="greetingMessage"
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
name="greetingMessage"
|
||||
error={
|
||||
touched.greetingMessage && Boolean(errors.greetingMessage)
|
||||
}
|
||||
helperText={
|
||||
touched.greetingMessage && errors.greetingMessage
|
||||
}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Field
|
||||
as={TextField}
|
||||
label={i18n.t("whatsappModal.form.farewellMessage")}
|
||||
type="farewellMessage"
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
name="farewellMessage"
|
||||
error={
|
||||
touched.farewellMessage && Boolean(errors.farewellMessage)
|
||||
}
|
||||
helperText={
|
||||
touched.farewellMessage && errors.farewellMessage
|
||||
}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
/>
|
||||
</div>
|
||||
<QueueSelect
|
||||
selectedQueueIds={selectedQueueIds}
|
||||
onChange={selectedIds => setSelectedQueueIds(selectedIds)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="secondary"
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
>
|
||||
{i18n.t("whatsappModal.buttons.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
variant="contained"
|
||||
className={classes.btnWrapper}
|
||||
>
|
||||
{whatsAppId
|
||||
? i18n.t("whatsappModal.buttons.okEdit")
|
||||
: i18n.t("whatsappModal.buttons.okAdd")}
|
||||
{isSubmitting && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
className={classes.buttonProgress}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WhatsAppModal);
|
||||
export default React.memo(WhatsAppModal)
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import React, { createContext } from "react";
|
||||
import React, { createContext } from 'react'
|
||||
|
||||
import useAuth from "../../hooks/useAuth.js";
|
||||
import useAuth from '../../hooks/useAuth.js'
|
||||
|
||||
const AuthContext = createContext();
|
||||
const AuthContext = createContext()
|
||||
|
||||
const AuthProvider = ({ children }) => {
|
||||
const { loading, user, isAuth, handleLogin, handleLogout } = useAuth();
|
||||
const { loading, user, isAuth, handleLogin, handleLogout, setSetting } =
|
||||
useAuth()
|
||||
|
||||
//{
|
||||
//{
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ loading, user, isAuth, handleLogin, handleLogout }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ loading, user, isAuth, handleLogin, handleLogout, setSetting }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { AuthContext, AuthProvider };
|
||||
export { AuthContext, AuthProvider }
|
||||
|
|
|
@ -1,125 +1,158 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import openSocket from "socket.io-client";
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import openSocket from 'socket.io-client'
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import api from "../../services/api";
|
||||
import toastError from "../../errors/toastError";
|
||||
import { i18n } from '../../translate/i18n'
|
||||
import api from '../../services/api'
|
||||
import toastError from '../../errors/toastError'
|
||||
|
||||
const useAuth = () => {
|
||||
const history = useHistory();
|
||||
const [isAuth, setIsAuth] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState({});
|
||||
const history = useHistory()
|
||||
const [isAuth, setIsAuth] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [user, setUser] = useState({})
|
||||
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${JSON.parse(token)}`;
|
||||
setIsAuth(true);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
const [setting, setSetting] = useState({})
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response;
|
||||
},
|
||||
async error => {
|
||||
const originalRequest = error.config;
|
||||
if (error?.response?.status === 403 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${JSON.parse(token)}`
|
||||
setIsAuth(true)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
const { data } = await api.post("/auth/refresh_token");
|
||||
if (data) {
|
||||
localStorage.setItem("token", JSON.stringify(data.token));
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
||||
}
|
||||
return api(originalRequest);
|
||||
}
|
||||
if (error?.response?.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
api.defaults.headers.Authorization = undefined;
|
||||
setIsAuth(false);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config
|
||||
if (error?.response?.status === 403 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
(async () => {
|
||||
if (token) {
|
||||
try {
|
||||
const { data } = await api.post("/auth/refresh_token");
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
||||
setIsAuth(true);
|
||||
setUser(data.user);
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
const { data } = await api.post('/auth/refresh_token')
|
||||
if (data) {
|
||||
localStorage.setItem('token', JSON.stringify(data.token))
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`
|
||||
}
|
||||
return api(originalRequest)
|
||||
}
|
||||
if (error?.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
api.defaults.headers.Authorization = undefined
|
||||
setIsAuth(false)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
;(async () => {
|
||||
if (token) {
|
||||
try {
|
||||
const { data } = await api.post('/auth/refresh_token')
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`
|
||||
setIsAuth(true)
|
||||
setUser(data.user)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
socket.on("user", data => {
|
||||
if (data.action === "update" && data.user.id === user.id) {
|
||||
setUser(data.user);
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
try {
|
||||
const { data } = await api.get('/settings')
|
||||
setSetting(data)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
fetchSession()
|
||||
}, [])
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [user]);
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
|
||||
|
||||
const handleLogin = async userData => {
|
||||
setLoading(true);
|
||||
socket.on('user', (data) => {
|
||||
if (data.action === 'update' && data.user.id === user.id) {
|
||||
setUser(data.user)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const { data } = await api.post("/auth/login", userData);
|
||||
localStorage.setItem("token", JSON.stringify(data.token));
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`;
|
||||
setUser(data.user);
|
||||
setIsAuth(true);
|
||||
toast.success(i18n.t("auth.toasts.success"));
|
||||
history.push("/tickets");
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
socket.on('settings', (data) => {
|
||||
if (data.action === 'update') {
|
||||
setSetting((prevState) => {
|
||||
const aux = [...prevState]
|
||||
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
|
||||
aux[settingIndex].value = data.setting.value
|
||||
return aux
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handleLogout = async () => {
|
||||
setLoading(true);
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
try {
|
||||
await api.delete("/auth/logout");
|
||||
setIsAuth(false);
|
||||
setUser({});
|
||||
localStorage.removeItem("token");
|
||||
api.defaults.headers.Authorization = undefined;
|
||||
setLoading(false);
|
||||
history.push("/login");
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleLogin = async (userData) => {
|
||||
setLoading(true)
|
||||
|
||||
return { isAuth, user, loading, handleLogin, handleLogout };
|
||||
};
|
||||
try {
|
||||
const { data } = await api.post('/auth/login', userData)
|
||||
localStorage.setItem('token', JSON.stringify(data.token))
|
||||
api.defaults.headers.Authorization = `Bearer ${data.token}`
|
||||
setUser(data.user)
|
||||
setIsAuth(true)
|
||||
toast.success(i18n.t('auth.toasts.success'))
|
||||
history.push('/tickets')
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
export default useAuth;
|
||||
const handleLogout = async () => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
await api.delete('/auth/logout')
|
||||
setIsAuth(false)
|
||||
setUser({})
|
||||
localStorage.removeItem('token')
|
||||
api.defaults.headers.Authorization = undefined
|
||||
setLoading(false)
|
||||
history.push('/login')
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isAuth,
|
||||
user,
|
||||
loading,
|
||||
handleLogin,
|
||||
handleLogout,
|
||||
setting,
|
||||
setSetting,
|
||||
}
|
||||
}
|
||||
|
||||
export default useAuth
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useReducer, useState, useContext } from "react";
|
||||
import React, { useEffect, useReducer, useState, useContext } from 'react'
|
||||
|
||||
import openSocket from "socket.io-client";
|
||||
import openSocket from 'socket.io-client'
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
@ -13,155 +13,186 @@ import {
|
|||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
} from '@material-ui/core'
|
||||
|
||||
import MainContainer from "../../components/MainContainer";
|
||||
import MainHeader from "../../components/MainHeader";
|
||||
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
|
||||
import TableRowSkeleton from "../../components/TableRowSkeleton";
|
||||
import Title from "../../components/Title";
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import toastError from "../../errors/toastError";
|
||||
import api from "../../services/api";
|
||||
import { DeleteOutline, Edit } from "@material-ui/icons";
|
||||
import QueueModal from "../../components/QueueModal";
|
||||
import { toast } from "react-toastify";
|
||||
import ConfirmationModal from "../../components/ConfirmationModal";
|
||||
import MainContainer from '../../components/MainContainer'
|
||||
import MainHeader from '../../components/MainHeader'
|
||||
import MainHeaderButtonsWrapper from '../../components/MainHeaderButtonsWrapper'
|
||||
import TableRowSkeleton from '../../components/TableRowSkeleton'
|
||||
import Title from '../../components/Title'
|
||||
import { i18n } from '../../translate/i18n'
|
||||
import toastError from '../../errors/toastError'
|
||||
import api from '../../services/api'
|
||||
import { DeleteOutline, Edit } from '@material-ui/icons'
|
||||
import QueueModal from '../../components/QueueModal'
|
||||
import { toast } from 'react-toastify'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
|
||||
import { AuthContext } from "../../context/Auth/AuthContext";
|
||||
import { Can } from "../../components/Can";
|
||||
import { AuthContext } from '../../context/Auth/AuthContext'
|
||||
import { Can } from '../../components/Can'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
mainPaper: {
|
||||
flex: 1,
|
||||
padding: theme.spacing(1),
|
||||
overflowY: "scroll",
|
||||
overflowY: 'scroll',
|
||||
...theme.scrollbarStyles,
|
||||
},
|
||||
customTableCell: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
}))
|
||||
|
||||
const reducer = (state, action) => {
|
||||
if (action.type === "LOAD_QUEUES") {
|
||||
const queues = action.payload;
|
||||
const newQueues = [];
|
||||
if (action.type === 'LOAD_QUEUES') {
|
||||
const queues = action.payload
|
||||
const newQueues = []
|
||||
|
||||
queues.forEach((queue) => {
|
||||
const queueIndex = state.findIndex((q) => q.id === queue.id);
|
||||
const queueIndex = state.findIndex((q) => q.id === queue.id)
|
||||
if (queueIndex !== -1) {
|
||||
state[queueIndex] = queue;
|
||||
state[queueIndex] = queue
|
||||
} else {
|
||||
newQueues.push(queue);
|
||||
newQueues.push(queue)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return [...state, ...newQueues];
|
||||
return [...state, ...newQueues]
|
||||
}
|
||||
|
||||
if (action.type === "UPDATE_QUEUES") {
|
||||
const queue = action.payload;
|
||||
const queueIndex = state.findIndex((u) => u.id === queue.id);
|
||||
if (action.type === 'UPDATE_QUEUES') {
|
||||
const queue = action.payload
|
||||
const queueIndex = state.findIndex((u) => u.id === queue.id)
|
||||
|
||||
if (queueIndex !== -1) {
|
||||
state[queueIndex] = queue;
|
||||
return [...state];
|
||||
state[queueIndex] = queue
|
||||
return [...state]
|
||||
} else {
|
||||
return [queue, ...state];
|
||||
return [queue, ...state]
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === "DELETE_QUEUE") {
|
||||
const queueId = action.payload;
|
||||
const queueIndex = state.findIndex((q) => q.id === queueId);
|
||||
if (action.type === 'DELETE_QUEUE') {
|
||||
const queueId = action.payload
|
||||
const queueIndex = state.findIndex((q) => q.id === queueId)
|
||||
if (queueIndex !== -1) {
|
||||
state.splice(queueIndex, 1);
|
||||
state.splice(queueIndex, 1)
|
||||
}
|
||||
return [...state];
|
||||
return [...state]
|
||||
}
|
||||
|
||||
if (action.type === "RESET") {
|
||||
return [];
|
||||
if (action.type === 'RESET') {
|
||||
return []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const Queues = () => {
|
||||
const classes = useStyles();
|
||||
const classes = useStyles()
|
||||
|
||||
const { user } = useContext(AuthContext);
|
||||
const { user } = useContext(AuthContext)
|
||||
|
||||
const [queues, dispatch] = useReducer(reducer, []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [queues, dispatch] = useReducer(reducer, [])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [queueModalOpen, setQueueModalOpen] = useState(false);
|
||||
const [selectedQueue, setSelectedQueue] = useState(null);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||
const [queueModalOpen, setQueueModalOpen] = useState(false)
|
||||
const [selectedQueue, setSelectedQueue] = useState(null)
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState(false)
|
||||
|
||||
const [settings, setSettings] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
;(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await api.get("/queue");
|
||||
dispatch({ type: "LOAD_QUEUES", payload: data });
|
||||
const { data } = await api.get('/queue')
|
||||
dispatch({ type: 'LOAD_QUEUES', payload: data })
|
||||
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
setLoading(false);
|
||||
toastError(err)
|
||||
setLoading(false)
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
const fetchSession = async () => {
|
||||
try {
|
||||
const { data } = await api.get('/settings')
|
||||
setSettings(data)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
fetchSession()
|
||||
}, [])
|
||||
|
||||
socket.on("queue", (data) => {
|
||||
if (data.action === "update" || data.action === "create") {
|
||||
dispatch({ type: "UPDATE_QUEUES", payload: data.queue });
|
||||
const getSettingValue = (key) => {
|
||||
const { value } = settings.find((s) => s.key === key)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
|
||||
|
||||
socket.on('queue', (data) => {
|
||||
if (data.action === 'update' || data.action === 'create') {
|
||||
dispatch({ type: 'UPDATE_QUEUES', payload: data.queue })
|
||||
}
|
||||
|
||||
if (data.action === "delete") {
|
||||
dispatch({ type: "DELETE_QUEUE", payload: data.queueId });
|
||||
if (data.action === 'delete') {
|
||||
dispatch({ type: 'DELETE_QUEUE', payload: data.queueId })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
socket.on('settings', (data) => {
|
||||
if (data.action === 'update') {
|
||||
setSettings((prevState) => {
|
||||
const aux = [...prevState]
|
||||
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
|
||||
aux[settingIndex].value = data.setting.value
|
||||
return aux
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOpenQueueModal = () => {
|
||||
setQueueModalOpen(true);
|
||||
setSelectedQueue(null);
|
||||
};
|
||||
setQueueModalOpen(true)
|
||||
setSelectedQueue(null)
|
||||
}
|
||||
|
||||
const handleCloseQueueModal = () => {
|
||||
setQueueModalOpen(false);
|
||||
setSelectedQueue(null);
|
||||
};
|
||||
setQueueModalOpen(false)
|
||||
setSelectedQueue(null)
|
||||
}
|
||||
|
||||
const handleEditQueue = (queue) => {
|
||||
setSelectedQueue(queue);
|
||||
setQueueModalOpen(true);
|
||||
};
|
||||
setSelectedQueue(queue)
|
||||
setQueueModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseConfirmationModal = () => {
|
||||
setConfirmModalOpen(false);
|
||||
setSelectedQueue(null);
|
||||
};
|
||||
setConfirmModalOpen(false)
|
||||
setSelectedQueue(null)
|
||||
}
|
||||
|
||||
const handleDeleteQueue = async (queueId) => {
|
||||
try {
|
||||
await api.delete(`/queue/${queueId}`);
|
||||
toast.success(i18n.t("Queue deleted successfully!"));
|
||||
await api.delete(`/queue/${queueId}`)
|
||||
toast.success(i18n.t('Queue deleted successfully!'))
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
toastError(err)
|
||||
}
|
||||
setSelectedQueue(null);
|
||||
};
|
||||
setSelectedQueue(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<Can
|
||||
|
@ -172,14 +203,15 @@ const Queues = () => {
|
|||
<ConfirmationModal
|
||||
title={
|
||||
selectedQueue &&
|
||||
`${i18n.t("queues.confirmationModal.deleteTitle")} ${selectedQueue.name
|
||||
`${i18n.t('queues.confirmationModal.deleteTitle')} ${
|
||||
selectedQueue.name
|
||||
}?`
|
||||
}
|
||||
open={confirmModalOpen}
|
||||
onClose={handleCloseConfirmationModal}
|
||||
onConfirm={() => handleDeleteQueue(selectedQueue.id)}
|
||||
>
|
||||
{i18n.t("queues.confirmationModal.deleteMessage")}
|
||||
{i18n.t('queues.confirmationModal.deleteMessage')}
|
||||
</ConfirmationModal>
|
||||
<QueueModal
|
||||
open={queueModalOpen}
|
||||
|
@ -187,8 +219,7 @@ const Queues = () => {
|
|||
queueId={selectedQueue?.id}
|
||||
/>
|
||||
<MainHeader>
|
||||
<Title>{i18n.t("queues.title")}</Title>
|
||||
|
||||
<Title>{i18n.t('queues.title')}</Title>
|
||||
|
||||
<Can
|
||||
role={user.profile}
|
||||
|
@ -200,29 +231,27 @@ const Queues = () => {
|
|||
color="primary"
|
||||
onClick={handleOpenQueueModal}
|
||||
>
|
||||
{i18n.t("queues.buttons.add")}
|
||||
{i18n.t('queues.buttons.add')}
|
||||
</Button>
|
||||
</MainHeaderButtonsWrapper>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
</MainHeader>
|
||||
<Paper className={classes.mainPaper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">
|
||||
{i18n.t("queues.table.name")}
|
||||
{i18n.t('queues.table.name')}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{i18n.t("queues.table.color")}
|
||||
{i18n.t('queues.table.color')}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{i18n.t("queues.table.greeting")}
|
||||
{i18n.t('queues.table.greeting')}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{i18n.t("queues.table.actions")}
|
||||
{i18n.t('queues.table.actions')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
@ -238,7 +267,7 @@ const Queues = () => {
|
|||
backgroundColor: queue.color,
|
||||
width: 60,
|
||||
height: 20,
|
||||
alignSelf: "center",
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -246,7 +275,7 @@ const Queues = () => {
|
|||
<TableCell align="center">
|
||||
<div className={classes.customTableCell}>
|
||||
<Typography
|
||||
style={{ width: 300, align: "center" }}
|
||||
style={{ width: 300, align: 'center' }}
|
||||
noWrap
|
||||
variant="body2"
|
||||
>
|
||||
|
@ -255,25 +284,44 @@ const Queues = () => {
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
|
||||
|
||||
<TableCell align="center">
|
||||
|
||||
|
||||
<Can
|
||||
role={user.profile}
|
||||
perform="show-icon-edit-queue"
|
||||
yes={() => (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleEditQueue(queue)}
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
display: 'inline',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
{(settings &&
|
||||
settings.length > 0 &&
|
||||
getSettingValue('editQueue') &&
|
||||
getSettingValue('editQueue') === 'enabled') |
|
||||
(user.profile === 'master') ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleEditQueue(queue)}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
// <IconButton
|
||||
// size="small"
|
||||
// onClick={() => handleEditQueue(queue)}
|
||||
// >
|
||||
// <Edit />
|
||||
// </IconButton>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<Can
|
||||
role={user.profile}
|
||||
perform="show-icon-delete-queue"
|
||||
|
@ -281,15 +329,14 @@ const Queues = () => {
|
|||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedQueue(queue);
|
||||
setConfirmModalOpen(true);
|
||||
setSelectedQueue(queue)
|
||||
setConfirmModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<DeleteOutline />
|
||||
</IconButton>
|
||||
)}
|
||||
/>
|
||||
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
@ -301,7 +348,7 @@ const Queues = () => {
|
|||
</MainContainer>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Queues;
|
||||
export default Queues
|
||||
|
|
|
@ -1,190 +1,207 @@
|
|||
import React, { useState, useEffect, useContext} from "react";
|
||||
import openSocket from "socket.io-client";
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import openSocket from 'socket.io-client'
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import Select from "@material-ui/core/Select";
|
||||
import { toast } from "react-toastify";
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Container from '@material-ui/core/Container'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import api from "../../services/api";
|
||||
import { i18n } from "../../translate/i18n.js";
|
||||
import toastError from "../../errors/toastError";
|
||||
import api from '../../services/api'
|
||||
import { i18n } from '../../translate/i18n.js'
|
||||
import toastError from '../../errors/toastError'
|
||||
|
||||
//--------
|
||||
import { AuthContext } from "../../context/Auth/AuthContext";
|
||||
import { Can } from "../../components/Can";
|
||||
import { AuthContext } from '../../context/Auth/AuthContext'
|
||||
|
||||
import { Can } from '../../components/Can'
|
||||
|
||||
// import Button from "@material-ui/core/Button";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(4),
|
||||
},
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(4),
|
||||
},
|
||||
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
settingOption: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
margin: {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
settingOption: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
margin: {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
}))
|
||||
|
||||
const Settings = () => {
|
||||
const classes = useStyles();
|
||||
const classes = useStyles()
|
||||
|
||||
//--------
|
||||
const { user } = useContext(AuthContext);
|
||||
//--------
|
||||
const { user } = useContext(AuthContext)
|
||||
|
||||
const [settings, setSettings] = useState([]);
|
||||
const [settings, setSettings] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
try {
|
||||
const { data } = await api.get("/settings");
|
||||
setSettings(data);
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
}
|
||||
};
|
||||
fetchSession();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
try {
|
||||
const { data } = await api.get('/settings')
|
||||
setSettings(data)
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
fetchSession()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
useEffect(() => {
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
|
||||
|
||||
socket.on("settings", data => {
|
||||
if (data.action === "update") {
|
||||
setSettings(prevState => {
|
||||
const aux = [...prevState];
|
||||
const settingIndex = aux.findIndex(s => s.key === data.setting.key);
|
||||
aux[settingIndex].value = data.setting.value;
|
||||
return aux;
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on('settings', (data) => {
|
||||
console.log('settings updated ----------------------------')
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
if (data.action === 'update') {
|
||||
setSettings((prevState) => {
|
||||
const aux = [...prevState]
|
||||
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
|
||||
aux[settingIndex].value = data.setting.value
|
||||
return aux
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handleChangeSetting = async e => {
|
||||
const selectedValue = e.target.value;
|
||||
const settingKey = e.target.name;
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
try {
|
||||
await api.put(`/settings/${settingKey}`, {
|
||||
value: selectedValue,
|
||||
});
|
||||
toast.success(i18n.t("settings.success"));
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
console.log('------> settings: ', settings)
|
||||
}, [settings])
|
||||
|
||||
const getSettingValue = key => {
|
||||
const { value } = settings.find(s => s.key === key);
|
||||
return value;
|
||||
};
|
||||
const handleChangeSetting = async (e) => {
|
||||
const selectedValue = e.target.value
|
||||
const settingKey = e.target.name
|
||||
|
||||
try {
|
||||
await api.put(`/settings/${settingKey}`, {
|
||||
value: selectedValue,
|
||||
})
|
||||
toast.success(i18n.t('settings.success'))
|
||||
} catch (err) {
|
||||
toastError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// const handleEdit = () => {
|
||||
const getSettingValue = (key) => {
|
||||
const { value } = settings.find((s) => s.key === key)
|
||||
|
||||
//
|
||||
return value
|
||||
}
|
||||
|
||||
// }
|
||||
return (
|
||||
<Can
|
||||
role={user.profile}
|
||||
perform="settings-view:show"
|
||||
yes={() => (
|
||||
<div>
|
||||
<div className={classes.root}>
|
||||
<Container className={classes.container} maxWidth="sm">
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{i18n.t('settings.title')}
|
||||
</Typography>
|
||||
|
||||
return (
|
||||
<Paper className={classes.paper}>
|
||||
<Typography variant="body1">
|
||||
{i18n.t('settings.settings.userCreation.name')}
|
||||
</Typography>
|
||||
|
||||
<Select
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
native
|
||||
id="userCreation-setting"
|
||||
name="userCreation"
|
||||
value={
|
||||
settings &&
|
||||
settings.length > 0 &&
|
||||
getSettingValue('userCreation')
|
||||
}
|
||||
className={classes.settingOption}
|
||||
onChange={handleChangeSetting}
|
||||
>
|
||||
<option value="enabled">
|
||||
{i18n.t('settings.settings.userCreation.options.enabled')}
|
||||
</option>
|
||||
<option value="disabled">
|
||||
{i18n.t('settings.settings.userCreation.options.disabled')}
|
||||
</option>
|
||||
</Select>
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Can
|
||||
role={user.profile}
|
||||
perform="settings-view:show"
|
||||
yes={() => (
|
||||
<div className={classes.root}>
|
||||
<Container className={classes.container} maxWidth="sm">
|
||||
<Paper className={classes.paper}>
|
||||
<Typography variant="body1">Editar ura</Typography>
|
||||
|
||||
<div>
|
||||
<Select
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
native
|
||||
id="editURA-setting"
|
||||
name="editURA"
|
||||
value={
|
||||
settings &&
|
||||
settings.length > 0 &&
|
||||
getSettingValue('editURA')
|
||||
}
|
||||
className={classes.settingOption}
|
||||
onChange={handleChangeSetting}
|
||||
>
|
||||
<option value="enabled">Ativado</option>
|
||||
<option value="disabled">Desativado</option>
|
||||
</Select>
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<div className={classes.root}>
|
||||
<Container className={classes.container} maxWidth="sm">
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{i18n.t("settings.title")}
|
||||
</Typography>
|
||||
<div className={classes.root}>
|
||||
<Container className={classes.container} maxWidth="sm">
|
||||
<Paper className={classes.paper}>
|
||||
<Typography variant="body1">Editar fila</Typography>
|
||||
|
||||
<Paper className={classes.paper}>
|
||||
<Typography variant="body1">
|
||||
{i18n.t("settings.settings.userCreation.name")}
|
||||
</Typography>
|
||||
<Select
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
native
|
||||
id="editQueue-setting"
|
||||
name="editQueue"
|
||||
value={
|
||||
settings &&
|
||||
settings.length > 0 &&
|
||||
getSettingValue('editQueue')
|
||||
}
|
||||
className={classes.settingOption}
|
||||
onChange={handleChangeSetting}
|
||||
>
|
||||
<option value="enabled">Ativado</option>
|
||||
<option value="disabled">Desativado</option>
|
||||
</Select>
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<Select
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
native
|
||||
id="userCreation-setting"
|
||||
name="userCreation"
|
||||
value={
|
||||
settings && settings.length > 0 && getSettingValue("userCreation")
|
||||
}
|
||||
className={classes.settingOption}
|
||||
onChange={handleChangeSetting}
|
||||
>
|
||||
<option value="enabled">
|
||||
{i18n.t("settings.settings.userCreation.options.enabled")}
|
||||
</option>
|
||||
<option value="disabled">
|
||||
{i18n.t("settings.settings.userCreation.options.disabled")}
|
||||
</option>
|
||||
</Select>
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
|
||||
{/* <div className={classes.root}>
|
||||
<Container className={classes.container} maxWidth="sm">
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Application name
|
||||
</Typography>
|
||||
|
||||
<Paper className={classes.paper}>
|
||||
|
||||
<Typography variant="body1">
|
||||
Estudio Face
|
||||
</Typography>
|
||||
|
||||
|
||||
<Button
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
id="applicationName-setting"
|
||||
name="applicationName"
|
||||
color="primary"
|
||||
onClick={(e) => handleEdit()}
|
||||
className={classes.settingOption}
|
||||
>
|
||||
{"EDIT"}
|
||||
</Button>
|
||||
|
||||
</Paper>
|
||||
|
||||
</Container>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
/>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
export default Settings
|
||||
|
|
|
@ -1,59 +1,58 @@
|
|||
const rules = {
|
||||
user: {
|
||||
static: [],
|
||||
},
|
||||
user: {
|
||||
static: [],
|
||||
},
|
||||
|
||||
admin: {
|
||||
static: [
|
||||
//"show-icon-edit-whatsapp",
|
||||
admin: {
|
||||
static: [
|
||||
'show-icon-edit-whatsapp',
|
||||
'show-icon-edit-queue',
|
||||
|
||||
"drawer-admin-items:view",
|
||||
"tickets-manager:showall",
|
||||
"user-modal:editProfile",
|
||||
"user-modal:editQueues",
|
||||
"ticket-options:deleteTicket",
|
||||
"contacts-page:deleteContact",
|
||||
"contacts-page:import-csv-contacts",
|
||||
"connections-view:show",
|
||||
"dashboard-view:show",
|
||||
"queues-view:show",
|
||||
"user-view:show",
|
||||
"ticket-report:show",
|
||||
'drawer-admin-items:view',
|
||||
'tickets-manager:showall',
|
||||
'user-modal:editProfile',
|
||||
'user-modal:editQueues',
|
||||
'ticket-options:deleteTicket',
|
||||
'contacts-page:deleteContact',
|
||||
'contacts-page:import-csv-contacts',
|
||||
'connections-view:show',
|
||||
'dashboard-view:show',
|
||||
'queues-view:show',
|
||||
'user-view:show',
|
||||
'ticket-report:show',
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
master: {
|
||||
static: [
|
||||
'url-remote-session:show',
|
||||
'show-icon-edit-whatsapp',
|
||||
'show-icon-add-queue',
|
||||
'show-icon-edit-queue',
|
||||
'show-icon-delete-queue',
|
||||
'space-disk-info:show',
|
||||
|
||||
master: {
|
||||
static: [
|
||||
'drawer-admin-items:view',
|
||||
'tickets-manager:showall',
|
||||
'user-modal:editProfile',
|
||||
'user-modal:editQueues',
|
||||
'ticket-options:deleteTicket',
|
||||
'contacts-page:deleteContact',
|
||||
'contacts-page:import-contacts',
|
||||
'contacts-page:import-csv-contacts',
|
||||
'connections-view:show',
|
||||
'dashboard-view:show',
|
||||
'queues-view:show',
|
||||
'user-view:show',
|
||||
'settings-view:show',
|
||||
'btn-add-user',
|
||||
'icon-remove-user',
|
||||
'btn-add-whatsapp',
|
||||
'btn-remove-whatsapp',
|
||||
'ticket-report:show',
|
||||
'connection-button:show',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
"show-icon-edit-whatsapp",
|
||||
"show-icon-add-queue",
|
||||
"show-icon-edit-queue",
|
||||
"show-icon-delete-queue",
|
||||
"space-disk-info:show",
|
||||
|
||||
|
||||
"drawer-admin-items:view",
|
||||
"tickets-manager:showall",
|
||||
"user-modal:editProfile",
|
||||
"user-modal:editQueues",
|
||||
"ticket-options:deleteTicket",
|
||||
"contacts-page:deleteContact",
|
||||
"contacts-page:import-contacts",
|
||||
"contacts-page:import-csv-contacts",
|
||||
"connections-view:show",
|
||||
"dashboard-view:show",
|
||||
"queues-view:show",
|
||||
"user-view:show",
|
||||
"settings-view:show",
|
||||
"btn-add-user",
|
||||
"icon-remove-user",
|
||||
"btn-add-whatsapp",
|
||||
"btn-remove-whatsapp",
|
||||
"ticket-report:show",
|
||||
"connection-button:show"
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default rules;
|
||||
export default rules
|
||||
|
|
Loading…
Reference in New Issue