Compare commits

..

2 Commits

Author SHA1 Message Date
gustavo-gsp cc12cafb99 merge changes, to merge the updated versions
Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias
2024-04-18 10:19:36 -03:00
gustavo-gsp 860d462d37 feat: add sound notifications and popOver for new tickets in waiting status, with option to enable in settings
Details:
- Implemented functionality to provide sound notifications and popOver alerts for new tickets in waiting status. Users can enable this feature in the settings.

feat: add average waiting time in the Waiting tab on the Tickets screen, with option to enable in settings

Details:
- Added functionality to display the average waiting time in the Waiting tab on the Tickets screen. Users can enable this feature in the settings.

feat: add ticket link in simple export directly from the reports table

Details:
- Implemented functionality to include the ticket link in the simple export directly from the reports table.

feat: remove report export options for supervisor profile

Details:
- Removed the report export options for the supervisor profile to restrict access.
2024-04-18 10:18:17 -03:00
8 changed files with 324 additions and 100 deletions

View File

@ -8,6 +8,7 @@
"watch": "tsc -w",
"start": "nodemon --expose-gc dist/server.js",
"dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts",
"dev": "nodemon --exec npm run dev:server",
"pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all",
"test": "NODE_ENV=test jest",
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all"

View File

@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "blockAudioVideoMedia",
value: "disabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "waitingTimeTickets",
value: "disabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "notificationTransferQueue",
value: "disabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@ -169,7 +169,7 @@ const verifyMediaMessage = async (
if (!media) {
throw new Error("ERR_WAPP_DOWNLOAD_MEDIA");
}
let mediaAuthorized = true;
let messageData = {
id: msg.id.id,
ticketId: ticket.id,
@ -183,7 +183,9 @@ const verifyMediaMessage = async (
phoneNumberId: msg?.phoneNumberId,
fromAgent: false
};
if(messageData.mediaType === 'video' || messageData.mediaType === 'audio' && getSettingValue('blockAudioVideoMedia')?.value === 'enabled'){
mediaAuthorized = false;
}
if (msg?.fromMe) {
messageData = { ...messageData, fromAgent: true };
}
@ -198,23 +200,36 @@ const verifyMediaMessage = async (
body: media.filename
};
}
try {
await writeFileAsync(
join(__dirname, "..", "..", "..", "..", "..", "public", media.filename),
media.data,
"base64"
);
} catch (err) {
Sentry.captureException(err);
logger.error(`There was an error: wbotMessageLitener.ts: ${err}`);
if(mediaAuthorized){
try {
await writeFileAsync(
join(__dirname, "..", "..", "..", "..", "..", "public", media.filename),
media.data,
"base64"
);
} catch (err) {
Sentry.captureException(err);
logger.error(`There was an error: wbotMessageLitener.ts: ${err}`);
}
}
}
await ticket.update({ lastMessage: msg.body || media.filename });
const newMessage = await CreateMessageService({ messageData });
return newMessage;
if(mediaAuthorized){
await ticket.update({ lastMessage: msg.body || media.filename });
const newMessage = await CreateMessageService({ messageData });
return newMessage;
}else{
if (ticket.status !== "queueChoice") {
botSendMessage(
ticket,
`Atenção! Mensagem ignorada, tipo de mídia não suportado.`
);
}
messageData.body = `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`;
messageData.mediaUrl = '';
await ticket.update({ lastMessage: `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`});
const newMessage = await CreateMessageService({ messageData });
console.log(`--------->>> Mensagem do tipo: ${messageData.mediaType}, ignorada!`)
}
};
// const verifyMediaMessage = async (
@ -412,13 +427,15 @@ const verifyQueue = async (
}
let body = "";
const io = getIO();
if (botOptions.length > 0) {
body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`;
} else {
body = `\u200e${choosenQueue.greetingMessage}`;
}
io.emit('notifyPeding', {data: {ticket, queue: choosenQueue}});
sendWhatsAppMessageSocket(ticket, body);
} else {
//test del transfere o atendimento se entrar na ura infinita

View File

@ -20,6 +20,9 @@ import useTickets from "../../hooks/useTickets"
import alertSound from "../../assets/sound.mp3"
import { AuthContext } from "../../context/Auth/AuthContext"
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({
tabContainer: {
overflowY: "auto",
@ -83,7 +86,7 @@ const NotificationsPopOver = () => {
const historyRef = useRef(history)
const { handleLogout } = useContext(AuthContext)
const [settings, setSettings] = useState([]);
// const [lastRef] = useState(+history.location.pathname.split("/")[2])
@ -110,7 +113,22 @@ const NotificationsPopOver = () => {
ticketIdRef.current = ticketIdUrl
}, [ticketIdUrl])
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSettings(data.settings)
} catch (err) {
toastError(err)
}
}
fetchSession()
}, [])
const getSettingValue = (key) => {
const { value } = settings.find((s) => s.key === key)
return value
}
useEffect(() => {
@ -255,49 +273,80 @@ const NotificationsPopOver = () => {
if (shouldNotNotificate) return
handleNotifications(data)
}
})
socket.on('notifyPeding', data =>{
if(settings?.length > 0 && getSettingValue('notificationTransferQueue') === 'enabled') handleNotifications("", data);
});
return () => {
socket.disconnect()
}
}, [user])
}, [user, settings])
const handleNotifications = data => {
const { message, contact, ticket } = data
const handleNotifications = (data, notify) => {
let isQueue = false;
if(!notify){
const { message, contact, ticket } = data
const options = {
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
icon: contact.profilePicUrl,
tag: ticket.id,
renotify: true,
}
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
)
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets/${ticket.id}`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
const options = {
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
icon: contact.profilePicUrl,
tag: ticket.id,
renotify: true,
}
return [notification, ...prevState]
})
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
)
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets/${ticket.id}`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
}
return [notification, ...prevState]
})
}else{
user.queues.forEach(queue =>{
if(queue.id === notify.data?.queue?.id){
isQueue = true;
}
})
if(!isQueue){
return;
}else {
const notification = new Notification(`${i18n.t("tickets.notification.messagePeding")} ${notify.data?.queue?.name}`);
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
}
return [notification, ...prevState]
})
}
}
soundAlertRef.current()
}

View File

@ -33,6 +33,9 @@ import { Button } from "@material-ui/core";
import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption";
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket";
import useTickets from "../../hooks/useTickets"
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({
ticketsWrapper: {
@ -157,6 +160,10 @@ const TicketsManager = () => {
const [openTooltipSearch, setOpenTooltipSearch] = useState(false)
const [waitingTime, setWaitingTime] = useState('00:00');
const [tickets, setTickets] = useState([]);
const [settings, setSettings] = useState([])
let searchTimeout;
let searchContentTimeout;
@ -178,6 +185,76 @@ const TicketsManager = () => {
}, [tab, setTabOption]);
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSettings(data.settings)
} catch (err) {
toastError(err)
}
}
fetchSession()
}, [])
const getSettingValue = (key) => {
const { value } = settings.find((s) => s.key === key)
return value
}
const fetchTickets = async () =>{
try {
const { data } = await api.get("/tickets", {
params: {
status: 'pending',
queueIds: JSON.stringify(selectedQueueIds)
},
});
setTickets(data.tickets);
} catch (err) {
toastError(err);
}
}
useEffect(() => {
if(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') {
fetchTickets();
const intervalId = setInterval(fetchTickets, 7000);
return () => {
clearInterval(intervalId);
};
}
}, [selectedQueueIds, settings]);
useEffect(() => {
const calculateAverageTime = () => {
if(tickets.length > 0){
const now = new Date();
const differenceTime = tickets?.map(ticket => {
const updatedAt = new Date(ticket.updatedAt);
const difference = now - updatedAt;
return difference;
});
const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0);
const averageTimeMilliseconds = sumDifferences / tickets?.length;
let hours = Math.floor(averageTimeMilliseconds / 3600000);
const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000);
let days = hours >= 24 ? parseInt(hours/24) : '';
if(days != '') hours = hours - (24*days);
const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}`;
return averageTimeFormated;
}else return '00:00';
}
setWaitingTime(calculateAverageTime());
},[tickets]);
useEffect(() => {
// clearTimeout(searchContentTimeout);
@ -203,7 +280,7 @@ const TicketsManager = () => {
useEffect(() => {
//console.log(selectedQueueIds);
if (tabOption === 'open') {
setTabOption('')
@ -448,7 +525,17 @@ const TicketsManager = () => {
</Badge>
}
value={"pending"}
/>
/>{
(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') &&
<span style={{display: 'flex', alignItems: 'center', flexDirection:'column', justifyContent: 'flex-start'}}>
<label style ={{color: 'red',fontWeight: 'bold', padding: '.1rem', fontSize: '8px', textAlign:'center', margin:'0'}}>
<i>ESPERA</i>
</label>
<label style={{color: 'gray',fontWeight: 'bold', padding: '.1rem', textDecoration: 'underline', fontSize: '13px'}}>
{waitingTime}
</label>
</span>
}
</Tabs>
<Paper className={classes.ticketsWrapper}>
<TicketsList

View File

@ -237,7 +237,8 @@ let columnsData = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
]
{ title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
]
let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
@ -252,6 +253,7 @@ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
{ title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
]
@ -376,9 +378,9 @@ const Report = () => {
const tickets = data.tickets.map(ticket => ({
...ticket,
messagesToFilter: ticket.messages.map(message => message.body).join(' '),
link: `${process.env.REACT_APP_FRONTEND_URL}/tickets/${ticket.id}`
}))
dispatchQ({ type: "LOAD_QUERY", payload: tickets })
console.log(tickets)
setHasMore(data.hasMore)
setTotalCountTickets(data.count)
setLoading(false)
@ -681,54 +683,56 @@ const Report = () => {
const renderSwitch = (param) => {
switch (param) {
case 'empty':
return (
<>
{query && query.length > 0 &&
<ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} />
}
{/* <Button
disabled={query && query.length > 0 ? false : true}
variant="contained"
color="primary"
onClick={(e) => {
handleCSVMessages()
}}
>
{"CSV ALL"}
</Button> */}
</>)
if(userA.profile !== 'supervisor'){
switch (param) {
case 'empty':
return (
<>
{query && query.length > 0 &&
<ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} />
}
{/* <Button
disabled={query && query.length > 0 ? false : true}
variant="contained"
color="primary"
onClick={(e) => {
handleCSVMessages()
}}
>
{"CSV ALL"}
</Button> */}
</>)
case 'pending' || 'processing':
return (
<>
<span>PROCESSING...</span>
</>)
case 'pending' || 'processing':
return (
<>
<span>PROCESSING...</span>
</>)
case 'success':
return (
<>
<Button
variant="contained"
color="primary"
onClick={(e) => {
handleCSVDownload(e)
}}
>
{'CSV DOWNLOAD'}
</Button>
</>)
case 'downloading':
return (
<>
<span>DOWNLOADING...</span>
</>)
case 'success':
return (
<>
<Button
variant="contained"
color="primary"
onClick={(e) => {
handleCSVDownload(e)
}}
>
{'CSV DOWNLOAD'}
</Button>
</>)
case 'downloading':
return (
<>
<span>DOWNLOADING...</span>
</>)
default:
return (<><span>WAITING...</span></>)
default:
return (<><span>WAITING...</span></>)
}
}
}
@ -839,7 +843,7 @@ const Report = () => {
<>
<MTable data={query}
columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper}
hasChild={true}
hasChild={userA.profile !== 'supervisor' ? true :false}
removeClickRow={false}
handleScroll={handleScroll}