finalização do recurso de encerramento de ticket automatico e expiração de ticket

pull/20/head
adriano 2023-08-08 12:09:03 -03:00
parent 417f947263
commit c8ea53a4bc
18 changed files with 1257 additions and 488 deletions

View File

@ -5,6 +5,8 @@ import AppError from "../errors/AppError";
import UpdateSettingService from "../services/SettingServices/UpdateSettingService";
import ListSettingsService from "../services/SettingServices/ListSettingsService";
import updateSettingTicket from "../services/SettingServices/UpdateSettingTicket";
import SettingTicket from "../models/SettingTicket";
export const index = async (req: Request, res: Response): Promise<Response> => {
// if (req.user.profile !== "master") {
@ -12,8 +14,37 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
// }
const settings = await ListSettingsService();
const outBusinessHours = await SettingTicket.findOne({
where: { key: "outBusinessHours" }
});
const ticketExpiration = await SettingTicket.findOne({
where: { key: "ticketExpiration" }
});
return res.status(200).json(settings);
return res.status(200).json({ settings, outBusinessHours, ticketExpiration });
};
export const updateTicketSettings = async (
req: Request,
res: Response
): Promise<Response> => {
const { outBusinessHours, ticketExpiration } = req.body;
if (outBusinessHours && Object.keys(outBusinessHours).length > 0) {
await updateSettingTicket({
...outBusinessHours,
key: "outBusinessHours"
});
}
if (ticketExpiration && Object.keys(ticketExpiration).length > 0) {
await updateSettingTicket({
...ticketExpiration,
key: "ticketExpiration"
});
}
return res.status(200).json({ outBusinessHours, ticketExpiration });
};
export const update = async (

View File

@ -14,6 +14,7 @@ import QuickAnswer from "../models/QuickAnswer";
import SchedulingNotify from "../models/SchedulingNotify";
import StatusChatEnd from "../models/StatusChatEnd";
import UserOnlineTime from "../models/UserOnlineTime";
import SettingTicket from "../models/SettingTicket";
// eslint-disable-next-line
const dbConfig = require("../config/database");
// import dbConfig from "../config/database";
@ -36,6 +37,7 @@ const models = [
SchedulingNotify,
StatusChatEnd,
UserOnlineTime,
SettingTicket
];
sequelize.addModels(models);

View File

@ -0,0 +1,46 @@
import { QueryInterface, DataTypes } from "sequelize"
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("SettingTickets", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: true
},
startTime: {
type: DataTypes.DATE,
allowNull: true
},
endTime: {
type: DataTypes.DATE,
allowNull: true
},
value: {
type: DataTypes.STRING,
allowNull: false
},
key: {
type: DataTypes.STRING,
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("SettingTickets");
}
}

View File

@ -0,0 +1,34 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"SettingTickets",
[
{
message: "",
startTime: new Date(),
endTime: new Date(),
value: "disabled",
key: "outBusinessHours",
createdAt: new Date(),
updatedAt: new Date()
},
{
message: "",
startTime: new Date(),
endTime: new Date(),
value: "disabled",
key: "ticketExpiration",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("SettingTickets", {});
}
};

View File

@ -0,0 +1,84 @@
import SettingTicket from "../models/SettingTicket";
import ListTicketTimeLife from "../services/TicketServices/ListTicketTimeLife";
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
import BotIsOnQueue from "./BotIsOnQueue";
import {
format as _format,
isWithinInterval,
parse,
subMinutes
} from "date-fns";
import ptBR from "date-fns/locale/pt-BR";
import { splitDateTime } from "./SplitDateTime";
const fsPromises = require("fs/promises");
const fs = require("fs");
let timer: any;
const AutoCloseTickets = async () => {
try {
// const botInfo = await BotIsOnQueue('botqueue')
// if (!botInfo.userIdBot) return
const ticketExpiration = await SettingTicket.findOne({
where: { key: "ticketExpiration" }
});
if (ticketExpiration && ticketExpiration.value == "enabled") {
const startTime = splitDateTime(
new Date(
_format(new Date(ticketExpiration.startTime), "yyyy-MM-dd HH:mm:ss", {
locale: ptBR
})
)
);
const seconds = timeStringToSeconds(startTime.fullTime);
console.log("Ticket seconds: ", seconds);
let tickets: any = await ListTicketTimeLife({
timeseconds: seconds,
status: "open"
});
console.log("tickets: ", tickets);
for (let i = 0; i < tickets.length; i++) {
await UpdateTicketService({
ticketData: { status: "closed", statusChatEnd: "FINALIZADO" },
ticketId: tickets[i].ticket_id,
msg: ticketExpiration.message
});
}
}
} catch (error) {
console.log("There was an error on try close the bot tickets: ", error);
}
};
function timeStringToSeconds(timeString: any) {
const [hours, minutes, seconds] = timeString.split(":").map(Number);
return hours * 3600 + minutes * 60 + seconds;
}
const schedule = async () => {
try {
clearInterval(timer);
await AutoCloseTickets();
} catch (error) {
console.log("error on schedule: ", error);
} finally {
timer = setInterval(schedule, 60000);
}
};
timer = setInterval(schedule, 60000);
export default schedule;

View File

@ -0,0 +1,40 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey,
AutoIncrement
} from "sequelize-typescript";
@Table
class SettingTicket extends Model<SettingTicket> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
message: string;
@Column
startTime: Date;
@Column
endTime: Date;
@Column
value: string;
@Column
key: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
}
export default SettingTicket;

View File

@ -9,7 +9,15 @@ settingRoutes.get("/settings", SettingController.index);
// routes.get("/settings/:settingKey", isAuth, SettingsController.show);
settingRoutes.put(
"/settings/ticket",
isAuth,
SettingController.updateTicketSettings
);
// change setting key to key in future
settingRoutes.put("/settings/:settingKey", isAuth, SettingController.update);
export default settingRoutes;

View File

@ -10,6 +10,7 @@ import { cacheSize, flushCache, loadTicketsCache } from "./helpers/TicketCache";
import { loadContactsCache } from "./helpers/ContactsCache";
import { loadSchedulesCache } from "./helpers/SchedulingNotifyCache";
import { delRestoreControllFile } from "./helpers/RestoreControll";
import "./helpers/AutoCloseTickets";
import "./helpers/SchedulingNotifySendMessage"
import axios from "axios";

View File

@ -0,0 +1,35 @@
import AppError from "../../errors/AppError";
import SettingTicket from "../../models/SettingTicket";
interface Request {
key: string;
startTime: string;
endTime: string;
value: string;
message: string;
}
const updateSettingTicket = async ({
key,
startTime,
endTime,
value,
message
}: Request): Promise<SettingTicket | undefined> => {
try {
const businessHours = await SettingTicket.findOne({ where: { key } });
if (!businessHours) {
throw new AppError("ERR_NO_SETTING_FOUND", 404);
}
await businessHours.update({ startTime, endTime, message, value });
return businessHours;
} catch (error: any) {
console.error("===> Error on UpdateSettingService.ts file: \n", error);
throw new AppError(error.message);
}
};
export default updateSettingTicket;

View File

@ -0,0 +1,47 @@
import { Sequelize, } from "sequelize";
const dbConfig = require("../../config/database");
const sequelize = new Sequelize(dbConfig);
const { QueryTypes } = require('sequelize');
import { splitDateTime } from "../../helpers/SplitDateTime";
import format from 'date-fns/format';
import ptBR from 'date-fns/locale/pt-BR';
interface Request {
timeseconds: string | number;
status: string;
userId?: string | number;
}
const ListTicketTimeLife = async ({timeseconds, status, userId }: Request): Promise<any[]> => {
let tickets = []
let currentDate = format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })
// console.log('------------------> currentDate: ', currentDate)
if (userId) {
// CONSULTANDO FILAS PELO ID DO USUARIO
tickets = await sequelize.query(`select user.id as user_id, user.name as user_name, t.id as ticket_id from Tickets as t inner join Users as user on
t.userId = user.id and user.name = 'botqueue' and t.status='${status}' and (TIMESTAMPDIFF(SECOND, t.updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT });
} else {
// CONSULTANDO FILAS PELO USUARIO
tickets = await sequelize.query(`select id as ticket_id from Tickets where status='${status}' and
(TIMESTAMPDIFF(SECOND, updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT });
}
return tickets;
};
export default ListTicketTimeLife;

View File

@ -63,7 +63,7 @@ const UpdateTicketService = async ({
await ticket.reload();
if (msg.length > 0) {
if (msg?.trim().length > 0) {
setTimeout(async () => {

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"dotenv": "^16.0.1",
"emoji-mart": "^3.0.1",
"formik": "^2.2.0",
"formik-material-ui-pickers": "^1.0.0-alpha.1",
"i18next": "^19.8.2",
"i18next-browser-languagedetector": "^6.0.1",
"js-file-download": "^0.4.12",

View File

@ -0,0 +1,304 @@
import React, { useState, useEffect, } 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 { TimePicker } from 'formik-material-ui-pickers'
import DateFnsUtils from '@date-io/date-fns'
import ptBrLocale from "date-fns/locale/pt-BR"
import {
MuiPickersUtilsProvider,
} from '@material-ui/pickers'
import {
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'
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexWrap: 'wrap',
},
multFieldLine: {
display: 'flex',
'& > *:not(:last-child)': {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: 'relative',
},
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'),
// })
const ConfigModal = ({ open, onClose, change }) => {
const classes = useStyles()
const initialState = {
startTimeBus: new Date(),
endTimeBus: new Date(),
messageBus: '',
businessTimeEnalbe: false,
ticketTimeExpiration: new Date(),
ticketExpirationMsg: '',
ticketExpirationEnable: false,
}
const [config, setConfig] = useState(initialState)
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setConfig({
startTimeBus: data.outBusinessHours.startTime,
endTimeBus: data.outBusinessHours.endTime,
messageBus: data.outBusinessHours.message,
businessTimeEnalbe: data.outBusinessHours.value === 'enabled' ? true : false,
ticketTimeExpiration: data.ticketExpiration.startTime,
ticketExpirationMsg: data.ticketExpiration.message,
ticketExpirationEnable: data.ticketExpiration.value === 'enabled' ? true : false
})
} catch (err) {
toastError(err)
}
}
fetchSession()
}, [change])
const handleSaveConfig = async (values) => {
values = {
outBusinessHours: {
startTime: values.startTimeBus,
endTime: values.endTimeBus,
message: values.messageBus,
value: values.businessTimeEnalbe ? 'enabled' : 'disabled'
},
ticketExpiration: {
startTime: values.ticketTimeExpiration,
message: values.ticketExpirationMsg,
value: values.ticketExpirationEnable ? 'enabled' : 'disabled'
}
}
try {
await api.put(`/settings/ticket`, values)
toast.success('Atualização realizada com sucesso!')
handleClose()
} catch (err) {
toastError(err)
}
}
const handleClose = () => {
onClose()
// setConfig(initialState)
}
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
scroll="paper"
>
<DialogTitle>
Configurações
</DialogTitle>
<Formik
initialValues={config}
enableReinitialize={true}
// validationSchema={SessionSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveConfig(values)
actions.setSubmitting(false)
}, 100)
}}
>
{({ values, touched, errors, isSubmitting }) => (
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={ptBrLocale}>
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
component={TimePicker}
name="startTimeBus"
label="Inicio atendimento"
ampm={false}
openTo="hours"
views={['hours', 'minutes',]}
format="HH:mm"
/>
{' '}
<Field
component={TimePicker}
name="endTimeBus"
label="Fim atendimento"
ampm={false}
openTo="hours"
views={['hours', 'minutes',]}
format="HH:mm"
/>
<FormControlLabel
control={
<Field
as={Switch}
color="primary"
name="businessTimeEnalbe"
checked={values.businessTimeEnalbe}
/>
}
label={'Ativar/Desativar'} />
</div>
<div>
<Field
as={TextField}
label={'Mensagem fora do horário de atendimento'}
type="messageBus"
multiline
rows={5}
fullWidth
name="messageBus"
error={
touched.messageBus && Boolean(errors.messageBus)
}
helperText={
touched.messageBus && errors.messageBus
}
variant="outlined"
margin="dense"
/>
</div>
<br />
<div className={classes.multFieldLine}>
<Field
component={TimePicker}
name="ticketTimeExpiration"
label="Ticket expira em hh:mm"
ampm={false}
openTo="hours"
views={['hours', 'minutes',]}
format="HH:mm"
/>
<FormControlLabel
control={
<Field
as={Switch}
color="primary"
name="ticketExpirationEnable"
checked={values.ticketExpirationEnable}
/>
}
label={'Ativar/Desativar'}
/>
</div>
<div>
<Field
as={TextField}
label={'Mensagem por falta de atividade no atendimento'}
type="ticketExpirationMsg"
multiline
rows={5}
fullWidth
name="ticketExpirationMsg"
error={
touched.ticketExpirationMsg && Boolean(errors.ticketExpirationMsg)
}
helperText={
touched.ticketExpirationMsg && errors.ticketExpirationMsg
}
variant="outlined"
margin="dense"
/>
</div>
</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}
>
{isSubmitting ? (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
) : 'Salvar'}
</Button>
</DialogActions>
</Form>
</MuiPickersUtilsProvider>
)}
</Formik>
</Dialog>
</div>
)
}
export default React.memo(ConfigModal)

View File

@ -76,7 +76,7 @@ const useAuth = () => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSetting(data)
setSetting(data.settings)
} catch (err) {
toastError(err)
}

View File

@ -6,6 +6,9 @@ import openSocket from 'socket.io-client'
import { makeStyles } from '@material-ui/core/styles'
import { green } from '@material-ui/core/colors'
import Settings from "@material-ui/icons/Settings";
import {
Button,
TableBody,
@ -47,6 +50,7 @@ import toastError from '../../errors/toastError'
//--------
import { AuthContext } from '../../context/Auth/AuthContext'
import { Can } from '../../components/Can'
import ConfigModal from '../../components/ConfigModal'
const useStyles = makeStyles((theme) => ({
mainPaper: {
@ -107,6 +111,7 @@ const Connections = () => {
const { whatsApps, loading } = useContext(WhatsAppsContext)
const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false)
const [configModalOpen, setConfigModalOpen] = useState(false)
const [qrModalOpen, setQrModalOpen] = useState(false)
const [selectedWhatsApp, setSelectedWhatsApp] = useState(null)
const [confirmModalOpen, setConfirmModalOpen] = useState(false)
@ -134,7 +139,7 @@ const Connections = () => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSettings(data)
setSettings(data.settings)
} catch (err) {
toastError(err)
}
@ -205,6 +210,13 @@ const Connections = () => {
setWhatsAppModalOpen(true)
}
const handleOpenConfigModal = () => {
setConfigModalOpen(true)
}
const handleCloseConfigModal = () => {
setConfigModalOpen(false)
}
const handleCloseWhatsAppModal = useCallback(() => {
setWhatsAppModalOpen(false)
setSelectedWhatsApp(null)
@ -307,17 +319,17 @@ const Connections = () => {
{(whatsApp.status === 'CONNECTED' ||
whatsApp.status === 'PAIRING' ||
whatsApp.status === 'TIMEOUT') && (
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => {
handleOpenConfirmationModal('disconnect', whatsApp.id)
}}
>
{i18n.t('connections.buttons.disconnect')}
</Button>
)}
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => {
handleOpenConfirmationModal('disconnect', whatsApp.id)
}}
>
{i18n.t('connections.buttons.disconnect')}
</Button>
)}
{whatsApp.status === 'OPENING' && (
<Button size="small" variant="outlined" disabled color="default">
{i18n.t('connections.buttons.connecting')}
@ -454,10 +466,24 @@ const Connections = () => {
whatsAppId={!qrModalOpen && selectedWhatsApp?.id}
/>
<ConfigModal
open={configModalOpen}
onClose={handleCloseConfigModal}
change={configModalOpen}
/>
<MainHeader>
<Title>{i18n.t('connections.title')}</Title>
<MainHeaderButtonsWrapper>
<Button
variant="contained"
color="primary"
onClick={handleOpenConfigModal}
>
<Settings/>
</Button>
<Can
role={user.profile}
perform="btn-add-whatsapp"
@ -664,8 +690,8 @@ const Connections = () => {
settings.length > 0 &&
getSettingValue('editURA') &&
getSettingValue('editURA') ===
'enabled') |
(user.profile === 'master') ? (
'enabled') |
(user.profile === 'master') ? (
<IconButton
size="small"
onClick={() =>

View File

@ -121,7 +121,7 @@ const Queues = () => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSettings(data)
setSettings(data.settings)
} catch (err) {
toastError(err)
}

View File

@ -52,7 +52,7 @@ const Settings = () => {
const fetchSession = async () => {
try {
const { data } = await api.get('/settings')
setSettings(data)
setSettings(data.settings)
} catch (err) {
toastError(err)
}