Alterações para atribuir chats iniciados pelo atendente a mesma fila que ele esta atribuido.

Alteração para remover caracteres especias ao inserir o numero do whatsapp.
Criação do nivel supervisor para gerenciar filas
adriano 2023-06-02 15:45:14 -03:00
parent 1246f59441
commit 53d3b2b09e
11 changed files with 233 additions and 31 deletions

View File

@ -3,7 +3,7 @@ const db = [
{ {
client_url: "http://localhost:8080", client_url: "http://localhost:8080",
db_conf: { db_conf: {
DB: "whaticket", DB: "whaticket_recrutamento",
DB_HOST: "localhost", DB_HOST: "localhost",
DB_USER: "whaticket", DB_USER: "whaticket",
DB_PASS: "strongpassword", DB_PASS: "strongpassword",

View File

@ -15,6 +15,10 @@ import User from "../models/User";
import { startWhoIsOnlineMonitor, stopWhoIsOnlineMonitor } from "../helpers/WhoIsOnlineMonitor" import { startWhoIsOnlineMonitor, stopWhoIsOnlineMonitor } from "../helpers/WhoIsOnlineMonitor"
import UserOnlineTIme from '../models/UserOnlineTime' import UserOnlineTIme from '../models/UserOnlineTime'
import CountTicketsByUserQueue from "../services/UserServices/CountTicketsByUserQueue";
import { splitDateTime } from "../helpers/SplitDateTime";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
type IndexQuery = { type IndexQuery = {
searchParam: string; searchParam: string;
@ -37,6 +41,9 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
for (var user of users) { for (var user of users) {
if (user.profile !== 'master') { if (user.profile !== 'master') {
if (req.user.profile == 'supervisor' && (user.profile == 'admin')) continue
auxUsers.push(user) auxUsers.push(user)
} }
} }
@ -137,14 +144,49 @@ export const update = async (
req: Request, req: Request,
res: Response res: Response
): Promise<Response> => { ): Promise<Response> => {
if (req.user.profile !== "admin" && req.user.profile !== "master") { if (req.user.profile !== "admin" && req.user.profile !== "master" && req.user.profile !== "supervisor") {
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { userId } = req.params; const { userId } = req.params;
const userData = req.body; const userData = req.body;
const user = await UpdateUserService({ userData, userId });
const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
const openByUserOnQueue: any[] = await CountTicketsByUserQueue({ startDate: dateToday.fullDate, endDate: dateToday.fullDate, status: 'open', clientChatStart: true, userId: userId })
// console.log('------> openByUserOnQueue: ', openByUserOnQueue)
// console.log()
// console.log('------> 1 userData.queueIds: ', userData.queueIds)
let userQueuesAttendance = []
if ((openByUserOnQueue && openByUserOnQueue.length) > 0) {
userQueuesAttendance = openByUserOnQueue.filter((e: any) => !userData.queueIds.includes(e.queueId))
if (userQueuesAttendance && userQueuesAttendance.length > 0) {
const queueInAttendance = userQueuesAttendance.map((e) => e.queueId)
const mergedSet = new Set([...userData.queueIds, ...queueInAttendance])
// Convert the Set back to an array
userData.queueIds = Array.from(mergedSet)
// console.log('------> 2 userData.queueIds: ', userData.queueIds)
}
}
// console.log('userQueuesAttendance: ', userQueuesAttendance)
// return res.status(200).json({});
let user: any = await UpdateUserService({ userData, userId });
const io = getIO(); const io = getIO();
io.emit("user", { io.emit("user", {
@ -152,6 +194,8 @@ export const update = async (
user user
}); });
user.userQueuesAttendance = userQueuesAttendance
return res.status(200).json(user); return res.status(200).json(user);
}; };

View File

@ -0,0 +1,26 @@
import Whatsapp from "../models/Whatsapp";
import ShowQueuesByUser from "../services/UserServices/ShowQueuesByUser";
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
async function whatsappQueueMatchingUserQueue(userId: number, whatsapp: Whatsapp) {
const userQueues = await ShowQueuesByUser({ profile: 'user', userId: userId });
if (!userQueues || userQueues && userQueues.length == 0) return
console.log('-----> userQueues: ', userQueues);
let whats: any = await ShowWhatsAppService(whatsapp.id);
if (!whats.queues || whats.queues && whats.queues.length == 0) return
const whatsappQueues = whats.queues.map((e: any) => e.dataValues.name);
console.log('-----> whatsappQueues: ', whatsappQueues);
const matchingQueue = userQueues.find(queue => whatsappQueues.includes(queue.name));
return matchingQueue
}
export default whatsappQueueMatchingUserQueue

View File

@ -14,6 +14,10 @@ import { splitDateTime } from "../../helpers/SplitDateTime";
import TicketEmiterSumOpenClosedByUser from "../../helpers/OnlineReporEmiterInfoByUser"; import TicketEmiterSumOpenClosedByUser from "../../helpers/OnlineReporEmiterInfoByUser";
import { createOrUpdateTicketCache } from '../../helpers/TicketCache' import { createOrUpdateTicketCache } from '../../helpers/TicketCache'
import ShowQueuesByUser from "../UserServices/ShowQueuesByUser";
import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService";
import Whatsapp from "../../models/Whatsapp";
import whatsappQueueMatchingUserQueue from "../../helpers/whatsappQueueMatchingUserQueue";
let flatten = require('flat') let flatten = require('flat')
@ -32,8 +36,14 @@ const CreateTicketService = async ({
try { try {
console.log('Create contact service........')
const defaultWhatsapp = await GetDefaultWhatsApp(userId); const defaultWhatsapp = await GetDefaultWhatsApp(userId);
const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp);
console.log('matchingQueue: ', matchingQueue)
const queueId = matchingQueue ? matchingQueue.queueId : undefined
await CheckContactOpenTickets(contactId); await CheckContactOpenTickets(contactId);
const { isGroup } = await ShowContactService(contactId); const { isGroup } = await ShowContactService(contactId);
@ -42,9 +52,12 @@ const CreateTicketService = async ({
contactId, contactId,
status, status,
isGroup, isGroup,
userId userId,
queueId
}); });
console.log('TICKET CREATED!')
const ticket = await Ticket.findByPk(id, { include: ["contact"] }); const ticket = await Ticket.findByPk(id, { include: ["contact"] });
if (!ticket) { if (!ticket) {
@ -54,7 +67,6 @@ const CreateTicketService = async ({
// console.log('CONTACT ticket.id: ', ticket.id) // console.log('CONTACT ticket.id: ', ticket.id)
// TEST DEL
try { try {
let jsonString = JSON.stringify(ticket); //convert to string to remove the sequelize specific meta data let jsonString = JSON.stringify(ticket); //convert to string to remove the sequelize specific meta data
@ -68,7 +80,6 @@ const CreateTicketService = async ({
} catch (error) { } 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 })))
@ -92,3 +103,6 @@ const CreateTicketService = async ({
}; };
export default CreateTicketService; export default CreateTicketService;

View File

@ -927,7 +927,7 @@ const handleMsgAck = async (msg_id: any, ack: any) => {
} }
await messageToUpdate.update({ ack }); await messageToUpdate.update({ ack });
console.log('ACK messageToUpdate: ', JSON.parse(JSON.stringify(messageToUpdate))) // console.log('ACK messageToUpdate: ', JSON.parse(JSON.stringify(messageToUpdate)))
io.to(messageToUpdate.ticketId.toString()).emit("appMessage", { io.to(messageToUpdate.ticketId.toString()).emit("appMessage", {
action: "update", action: "update",

View File

@ -60,11 +60,13 @@ const ContactSchema = Yup.object().shape({
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"), number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().min(2, "Too Short!") email: Yup.string().min(2, "Too Short!")
.max(50, "Too Long!"), .max(50, "Too Long!"),
// email: Yup.string().email("Invalid email"), // email: Yup.string().email("Invalid email"),
}); });
const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
const classes = useStyles(); const classes = useStyles();
const isMounted = useRef(true); const isMounted = useRef(true);
@ -76,6 +78,8 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
}; };
const [contact, setContact] = useState(initialState); const [contact, setContact] = useState(initialState);
const [phone, setPhone] = useState('')
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -83,9 +87,22 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
}; };
}, []); }, []);
// useEffect(() => {
// console.log('1 Contact: ', contact)
// setContact({
// ...contact,
// number: phone,
// });
// console.log('2 Contact: ', contact)
// }, [phone]);
useEffect(() => { useEffect(() => {
const fetchContact = async () => { const fetchContact = async () => {
if (initialValues) { if (initialValues) {
setContact(prevState => { setContact(prevState => {
return { ...prevState, ...initialValues }; return { ...prevState, ...initialValues };
}); });
@ -95,9 +112,14 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
try { try {
const { data } = await api.get(`/contacts/${contactId}`); const { data } = await api.get(`/contacts/${contactId}`);
if (isMounted.current) { if (isMounted.current) {
setPhone(data.number)
setContact(data); setContact(data);
} }
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
@ -112,6 +134,11 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
}; };
const handleSaveContact = async values => { const handleSaveContact = async values => {
values = { ...values, number: phone };
console.log('submit values', values)
try { try {
if (contactId) { if (contactId) {
await api.put(`/contacts/${contactId}`, values); await api.put(`/contacts/${contactId}`, values);
@ -129,6 +156,28 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
} }
}; };
const handleChange = (event) => {
const regex = /^[0-9\b]+$/; // Regular expression to match only numbers
setPhone(event.target.value);
setTimeout(() => {
let newValue = ''
for (const char of event.target.value) {
if (char.match(regex)) {
newValue += char
}
}
setPhone(newValue)
}, 100);
};
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper"> <Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
@ -169,6 +218,10 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
as={TextField} as={TextField}
label={i18n.t("contactModal.form.number")} label={i18n.t("contactModal.form.number")}
name="number" name="number"
value={phone}
onChange={handleChange}
error={touched.number && Boolean(errors.number)} error={touched.number && Boolean(errors.number)}
helperText={touched.number && errors.number} helperText={touched.number && errors.number}
placeholder="5513912344321" placeholder="5513912344321"

View File

@ -75,7 +75,11 @@ const NewTicketModal = ({ modalOpen, onClose }) => {
userId: user.id, userId: user.id,
status: "open", status: "open",
}); });
window.location.reload();
history.push(`/tickets/${ticket.id}`); history.push(`/tickets/${ticket.id}`);
window.location.reload();
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }

View File

@ -18,7 +18,7 @@ import {
TextField, TextField,
InputAdornment, InputAdornment,
IconButton IconButton
} from '@material-ui/core'; } from '@material-ui/core';
import { Visibility, VisibilityOff } from '@material-ui/icons'; import { Visibility, VisibilityOff } from '@material-ui/icons';
@ -71,8 +71,8 @@ const UserSchema = Yup.object().shape({
password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"),
email: Yup.string().min(2, "Too Short!") email: Yup.string().min(2, "Too Short!")
.max(50, "Too Long!") .max(50, "Too Long!")
.required("Required"), .required("Required"),
// email: Yup.string().email("Invalid email").required("Required"), // email: Yup.string().email("Invalid email").required("Required"),
}); });
@ -98,12 +98,20 @@ const UserModal = ({ open, onClose, userId }) => {
const fetchUser = async () => { const fetchUser = async () => {
if (!userId) return; if (!userId) return;
try { try {
// console.log('window.location.href: ',window.location.href)
const { data } = await api.get(`/users/${userId}`); const { data } = await api.get(`/users/${userId}`);
setUser(prevState => { setUser(prevState => {
return { ...prevState, ...data }; return { ...prevState, ...data };
}); });
const userQueueIds = data.queues?.map(queue => queue.id); const userQueueIds = data.queues?.map(queue => queue.id);
setSelectedQueueIds(userQueueIds); setSelectedQueueIds(userQueueIds);
// console.log('data: ', data)
// console.log('loggedInUser.email: ', loggedInUser.email)
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
@ -121,7 +129,29 @@ const UserModal = ({ open, onClose, userId }) => {
const userData = { ...values, queueIds: selectedQueueIds }; const userData = { ...values, queueIds: selectedQueueIds };
try { try {
if (userId) { if (userId) {
await api.put(`/users/${userId}`, userData);
const user = await api.put(`/users/${userId}`, userData);
// console.log('USER: ', user.data)
if (user && user.data.userQueuesAttendance.length > 0) {
const userQueueInAttendance = user.data.userQueuesAttendance.map((e) => ({ "queue": e.queueName, "open": e.totAttendance }))
console.log('userQueueInAttendance: ', userQueueInAttendance)
let msg = '\nAVISO \n\nO atendente possui atendimento(s) em aberto na(s) fila(s) abaixo: \n\n'
userQueueInAttendance.forEach((e) => {
msg += `Fila: ${e.queue}\nAberto: ${e.open}\n\n`
})
msg += 'Para remover o atendente da(s) fila(s) acima é necessário que o mesmo encerre os atendimentos aberto(s) nessas filas.\n'
alert(msg)
}
} else { } else {
await api.post("/users", userData); await api.post("/users", userData);
} }
@ -166,6 +196,7 @@ const UserModal = ({ open, onClose, userId }) => {
label={i18n.t("userModal.form.name")} label={i18n.t("userModal.form.name")}
autoFocus autoFocus
name="name" name="name"
disabled={loggedInUser.profile === 'admin' || loggedInUser.profile === 'master' ? false : true}
error={touched.name && Boolean(errors.name)} error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name} helperText={touched.name && errors.name}
variant="outlined" variant="outlined"
@ -175,6 +206,7 @@ const UserModal = ({ open, onClose, userId }) => {
<Field <Field
as={TextField} as={TextField}
name="password" name="password"
disabled={loggedInUser.profile === 'admin' || loggedInUser.profile === 'master' ? false : true}
variant="outlined" variant="outlined"
margin="dense" margin="dense"
label={i18n.t("userModal.form.password")} label={i18n.t("userModal.form.password")}
@ -182,16 +214,16 @@ const UserModal = ({ open, onClose, userId }) => {
helperText={touched.password && errors.password} helperText={touched.password && errors.password}
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword((e) => !e)} onClick={() => setShowPassword((e) => !e)}
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
) )
}} }}
fullWidth fullWidth
/> />
@ -201,6 +233,7 @@ const UserModal = ({ open, onClose, userId }) => {
as={TextField} as={TextField}
label={i18n.t("userModal.form.email")} label={i18n.t("userModal.form.email")}
name="email" name="email"
disabled={loggedInUser.profile === 'admin' || loggedInUser.profile === 'master' ? false : true}
error={touched.email && Boolean(errors.email)} error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email} helperText={touched.email && errors.email}
variant="outlined" variant="outlined"
@ -230,6 +263,7 @@ const UserModal = ({ open, onClose, userId }) => {
required required
> >
<MenuItem value="admin">Admin</MenuItem> <MenuItem value="admin">Admin</MenuItem>
<MenuItem value="supervisor">Supervisor</MenuItem>
<MenuItem value="user">User</MenuItem> <MenuItem value="user">User</MenuItem>
</Field> </Field>
</> </>

View File

@ -94,9 +94,11 @@ const MainListItems = (props) => {
primary={i18n.t("mainDrawer.listItems.quickAnswers")} primary={i18n.t("mainDrawer.listItems.quickAnswers")}
icon={<QuestionAnswerOutlinedIcon />} icon={<QuestionAnswerOutlinedIcon />}
/> />
<Can <Can
role={user.profile} role={user.profile}
perform="drawer-admin-items:view" perform="menu-users:view"
yes={() => ( yes={() => (
<> <>
<Divider /> <Divider />
@ -106,6 +108,23 @@ const MainListItems = (props) => {
primary={i18n.t("mainDrawer.listItems.users")} primary={i18n.t("mainDrawer.listItems.users")}
icon={<PeopleAltOutlinedIcon />} icon={<PeopleAltOutlinedIcon />}
/> />
</>
)}
/>
<Can
role={user.profile}
perform="drawer-admin-items:view"
yes={() => (
<>
{/* <Divider />
<ListSubheader inset>{i18n.t("mainDrawer.listItems.administration")}</ListSubheader> */}
{/* <ListItemLink
to="/users"
primary={i18n.t("mainDrawer.listItems.users")}
icon={<PeopleAltOutlinedIcon />}
/> */}
<ListItemLink <ListItemLink
to="/queues" to="/queues"
primary={i18n.t("mainDrawer.listItems.queues")} primary={i18n.t("mainDrawer.listItems.queues")}

View File

@ -3,6 +3,14 @@ const rules = {
static: [], static: [],
}, },
supervisor: {
static: [
"menu-users:view",
"user-view:show",
"user-modal:editQueues"
]
},
admin: { admin: {
static: [ static: [
//"show-icon-edit-whatsapp", //"show-icon-edit-whatsapp",
@ -19,6 +27,7 @@ const rules = {
"queues-view:show", "queues-view:show",
"user-view:show", "user-view:show",
"ticket-report:show", "ticket-report:show",
"menu-users:view"
], ],
}, },
@ -31,8 +40,6 @@ const rules = {
"show-icon-edit-queue", "show-icon-edit-queue",
"show-icon-delete-queue", "show-icon-delete-queue",
"space-disk-info:show", "space-disk-info:show",
"drawer-admin-items:view", "drawer-admin-items:view",
"tickets-manager:showall", "tickets-manager:showall",
"user-modal:editProfile", "user-modal:editProfile",
@ -51,7 +58,8 @@ const rules = {
"btn-add-whatsapp", "btn-add-whatsapp",
"btn-remove-whatsapp", "btn-remove-whatsapp",
"ticket-report:show", "ticket-report:show",
"connection-button:show" "connection-button:show",
"menu-users:view"
], ],
}, },
}; };

View File

@ -280,7 +280,7 @@ const messages = {
}, },
newTicketModal: { newTicketModal: {
title: "Criar Ticket", title: "Criar Ticket",
fieldLabel: "Digite para pesquisar o contato", fieldLabel: "Digite o nome para pesquisar o contato",
add: "Adicionar", add: "Adicionar",
buttons: { buttons: {
ok: "Salvar", ok: "Salvar",