diff --git a/backend/package.json b/backend/package.json index 61d065e..6098b4e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,7 @@ "sequelize-cli": "^5.5.1", "sequelize-typescript": "^1.1.0", "socket.io": "^3.0.5", - "whatsapp-web.js": "^1.15.5", + "whatsapp-web.js": "^1.15.6", "yup": "^0.32.8" }, "devDependencies": { diff --git a/backend/src/controllers/SchedulingNotifyController.ts b/backend/src/controllers/SchedulingNotifyController.ts new file mode 100644 index 0000000..4185686 --- /dev/null +++ b/backend/src/controllers/SchedulingNotifyController.ts @@ -0,0 +1,15 @@ +import { Request, Response } from "express"; + +import DeleteSchedulingNotifyService from "../services/SchedulingNotifyServices/DeleteSchedulingNotifyService"; + + +export const remove = async ( req: Request, res: Response ): Promise => { + + console.log('EEEEEEEEEEEEEEEEEEEEEEEEEEE') + + const { scheduleId } = req.params; + + await DeleteSchedulingNotifyService(scheduleId); + + return res.status(200).send(); +}; diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 643141b..639de72 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -10,6 +10,7 @@ import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage"; import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; import CreateSchedulingNotifyService from "../services/SchedulingNotifyServices/CreateSchedulingNotifyService"; +import ListSchedulingNotifyContactService from "../services/SchedulingNotifyServices/ListSchedulingNotifyContactService"; @@ -94,9 +95,16 @@ export const show = async (req: Request, res: Response): Promise => { const { schedules, count, hasMore } = await ListScheduleService({ searchParam: "", pageNumber: "1" }); + ////////////////// + const schedulesContact = await ListSchedulingNotifyContactService(contact.contact.number); + ///////////////// + + // console.log('############### schedulesContact: ', schedulesContact) + // console.log('############### contact.contact.number: ',contact.contact.number) + // return res.status(200).json(contact); - return res.status(200).json({contact, schedules}); + return res.status(200).json({contact, schedules, schedulesContact}); }; @@ -134,12 +142,13 @@ export const update = async ( req: Request, res: Response ): Promise = // agendamento const scheduleData = JSON.parse(schedulingNotifyData) - if( scheduleData.scheduleId != '1'){ + if( scheduleData.scheduleId === '2'){ const schedulingNotifyCreate = await CreateSchedulingNotifyService( { ticketId: scheduleData.ticketId, scheduleId: scheduleData.scheduleId, schedulingDate: scheduleData.schedulingDate, + schedulingTime: scheduleData.schedulingTime, message: scheduleData.message } ) diff --git a/backend/src/database/migrations/20220224003939-create-scheduling-notify.ts b/backend/src/database/migrations/20220224003939-create-scheduling-notify.ts index 765d4d1..1d65dd1 100644 --- a/backend/src/database/migrations/20220224003939-create-scheduling-notify.ts +++ b/backend/src/database/migrations/20220224003939-create-scheduling-notify.ts @@ -27,6 +27,10 @@ module.exports = { type: DataTypes.DATE, allowNull: false }, + schedulingTime: { + type: DataTypes.DATE, + allowNull: false + }, message: { type: DataTypes.STRING, allowNull: false diff --git a/backend/src/models/SchedulingNotify.ts b/backend/src/models/SchedulingNotify.ts index c8eff6d..af94607 100644 --- a/backend/src/models/SchedulingNotify.ts +++ b/backend/src/models/SchedulingNotify.ts @@ -38,6 +38,9 @@ import { @Column schedulingDate: Date; + @Column + schedulingTime: Date; + @Column message: string diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index 41ae739..72370e5 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -78,8 +78,8 @@ class Ticket extends Model { @HasMany(() => Message) messages: Message[]; - @HasOne(() => SchedulingNotify) - schedulingNotify: SchedulingNotify + @HasMany(() => SchedulingNotify) + schedulingNotify: SchedulingNotify[]; } export default Ticket; diff --git a/backend/src/routes/SchedulingNotifyRoutes.ts b/backend/src/routes/SchedulingNotifyRoutes.ts new file mode 100644 index 0000000..975b5a1 --- /dev/null +++ b/backend/src/routes/SchedulingNotifyRoutes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import isAuth from "../middleware/isAuth"; + +import * as SchedulingNotifyController from "../controllers/SchedulingNotifyController"; + +const schedulingNotifiyRoutes = Router(); + +schedulingNotifiyRoutes.delete("/schedule/:scheduleId", isAuth, SchedulingNotifyController.remove); + +export default schedulingNotifiyRoutes; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index ab2d36e..98bcf6b 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -11,6 +11,7 @@ import whatsappSessionRoutes from "./whatsappSessionRoutes"; import queueRoutes from "./queueRoutes"; import quickAnswerRoutes from "./quickAnswerRoutes"; import reportRoutes from "./reportRoutes"; +import schedulingNotifiyRoutes from "./SchedulingNotifyRoutes"; const routes = Router(); @@ -26,6 +27,7 @@ routes.use(whatsappSessionRoutes); routes.use(queueRoutes); routes.use(quickAnswerRoutes); +routes.use(schedulingNotifiyRoutes) routes.use(reportRoutes); diff --git a/backend/src/services/SchedulingNotifyServices/CreateSchedulingNotifyService.ts b/backend/src/services/SchedulingNotifyServices/CreateSchedulingNotifyService.ts index 8f6252e..70ec9e1 100644 --- a/backend/src/services/SchedulingNotifyServices/CreateSchedulingNotifyService.ts +++ b/backend/src/services/SchedulingNotifyServices/CreateSchedulingNotifyService.ts @@ -6,6 +6,7 @@ interface Request { ticketId: string, scheduleId: string, schedulingDate: string, + schedulingTime: string, message: string } @@ -15,6 +16,7 @@ const CreateSchedulingNotifyService = async ( ticketId, scheduleId, schedulingDate, + schedulingTime, message }: Request): Promise => { @@ -24,6 +26,7 @@ const CreateSchedulingNotifyService = async ( ticketId, scheduleId, schedulingDate, + schedulingTime, message }) diff --git a/backend/src/services/SchedulingNotifyServices/ListSchedulingNotifyContactService.ts b/backend/src/services/SchedulingNotifyServices/ListSchedulingNotifyContactService.ts new file mode 100644 index 0000000..2a20ee0 --- /dev/null +++ b/backend/src/services/SchedulingNotifyServices/ListSchedulingNotifyContactService.ts @@ -0,0 +1,44 @@ +import Ticket from "../../models/Ticket"; +import Contact from "../../models/Contact"; +import SchedulingNotify from "../../models/SchedulingNotify"; +import { Op, where, Sequelize } from "sequelize"; +import AppError from "../../errors/AppError"; + +const ListSchedulingNotifyContactService = async (contactNumber: string): Promise => { + + + const ticket = await SchedulingNotify.findAll({ + + attributes:['id', [Sequelize.fn("DATE_FORMAT",Sequelize.col("schedulingDate"),"%d/%m/%Y %H:%i:%s"),"schedulingDate"], + [Sequelize.fn("DATE_FORMAT",Sequelize.col("schedulingTime"),"%d/%m/%Y %H:%i:%s"),"schedulingTime"], 'message'], + + include: [ + { + model: Ticket, + required:true, + attributes: [], + include: [ + { + model: Contact, + where:{ + number: contactNumber, + }, + attributes: ['name', 'number'] + }, + ] + }, + + ], + + }); + + + if (!ticket) { + throw new AppError("ERR_NO_TICKET_FOUND", 404); + } + + return ticket; + }; + + export default ListSchedulingNotifyContactService; + \ No newline at end of file diff --git a/frontend/src/components/ChatEnd/ModalChatEnd/index.js b/frontend/src/components/ChatEnd/ModalChatEnd/index.js index db9d91b..64036ee 100644 --- a/frontend/src/components/ChatEnd/ModalChatEnd/index.js +++ b/frontend/src/components/ChatEnd/ModalChatEnd/index.js @@ -1,27 +1,73 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useReducer } from 'react'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; - +import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; - - +import DialogTitle from '@mui/material/DialogTitle'; import PropTypes from 'prop-types'; -import Box from '@mui/material/Box'; - -import SelectField from "../../Report/SelectField"; -import DatePicker from '../../Report/DatePicker' - -import TimerPickerSelect from '../TimerPickerSelect' +import Box from '@mui/material/Box'; +import SelectField from "../../Report/SelectField"; +import TextFieldSelectHourBefore from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import DatePicker from '../../Report/DatePicker' +import TimerPickerSelect from '../TimerPickerSelect' +import TextareaAutosize from '@mui/material/TextareaAutosize'; +import { subHours } from "date-fns"; -import TextareaAutosize from '@mui/material/TextareaAutosize'; +import { + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@material-ui/core"; + +import { DeleteOutline } from "@material-ui/icons"; +import { toast } from "react-toastify"; import api from "../../../services/api"; +import toastError from "../../../errors/toastError"; +import ConfirmationModal from "../../ConfirmationModal"; -import { addHours } from "date-fns"; + +const reducer = (state, action) => { + + + if (action.type === "LOAD_SCHEDULES") { + const schedulesContact = action.payload; + const newSchedules= []; + + schedulesContact.forEach((schedule) => { + const scheduleIndex = state.findIndex((s) => s.id === schedule.id); + if (scheduleIndex !== -1) { + state[scheduleIndex] = schedule; + } else { + newSchedules.push(schedule); + } + }); + + return [...state, ...newSchedules]; + } + + if (action.type === "DELETE_SCHEDULE") { + const scheduleId = action.payload; + const scheduleIndex = state.findIndex((q) => q.id === scheduleId); + if (scheduleIndex !== -1) { + state.splice(scheduleIndex, 1); + } + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + + const Item = (props) => { @@ -56,9 +102,13 @@ Item.propTypes = { ]), }; + -const Modal = (props) => { +const Modal = (props) => { + // const [clientSchedules, dispatch] = useReducer(reducer, []); + const [selectedSchedule, setSelectedSchedule] = useState(null); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [open, setOpen] = useState(true); const [scroll, /*setScroll*/] = useState('body'); @@ -70,6 +120,11 @@ const Modal = (props) => { const [data] = useState(props.schedules) + const [schedulesContact, dispatch] = useReducer(reducer, []); + + const [currencyHourBefore, setCurrency] = useState(null); + const [currenciesTimeBefore, setCurrenciesTimeBefore] = useState(null); + const handleCancel = (event, reason) => { if (reason && reason === "backdropClick") @@ -80,6 +135,23 @@ const Modal = (props) => { + useEffect(() => { + + (async () => { + try { + + const { data } = await api.get("/tickets/" + props.ticketId); + + dispatch({ type: "LOAD_SCHEDULES", payload: data.schedulesContact }); + + } catch (err) { + toastError(err); + } + })(); + }, [props]); + + + function greetMessageSchedule(scheduleDate){ return `podemos confirmar sua consulta agendada para hoje às ${scheduleDate}?` } @@ -87,18 +159,40 @@ const Modal = (props) => { function formatedTimeHour(timer){ return `${timer.getHours().toString().padStart(2, '0')}:${timer.getMinutes().toString().padStart(2, '0')}` } + + + const handleCloseConfirmationModal = () => { + setConfirmModalOpen(false); + setSelectedSchedule(null); + }; + + + const handleDeleteSchedule = async (scheduleId) => { + try { + await api.delete(`/schedule/${scheduleId}`); + toast.success(("Agendamento deletado com sucesso!")); + dispatch({ type: "DELETE_SCHEDULE", payload: scheduleId }); + } catch (err) { + toastError(err); + } + setSelectedSchedule(null); + }; // Get from child 2 const datePickerValue = (data) => { console.log('datePickerValue: ',(data)); setDatePicker(data) + + } // Get from child 3 const timerPickerValue = (data) => { console.log('timerPickerValue: ',(data)); setTimerPicker(data) + + } const dateCurrentFormated = () => { @@ -113,38 +207,38 @@ const dateCurrentFormated = () => { } const handleChatEnd = (event, reason) => { + if (reason && reason === "backdropClick") return; if (scheduleId === '1'){ } - else if(textArea1.trim().length<10){ + else if(textArea1 && textArea1.trim().length<10){ alert('Mensagem muito curta!\nMínimo 10 caracteres.') return } - else if((new Date(timerPicker).getHours() > 16 && new Date(timerPicker).getMinutes() > 0) || + else if((new Date(timerPicker).getHours() > 20 && new Date(timerPicker).getMinutes() > 0) || (new Date(timerPicker).getHours() < 7)){ - alert('Horário comercial inválido!\n Selecione um horário de lembrete válido entre às 07:00 e 17:00') + alert('Horário comercial inválido!\n Selecione um horário de lembrete válido entre às 07:00 e 20:00') return } - else if(startDate === dateCurrentFormated()){ + else if(!currencyHourBefore){ + + alert('Para agendamentos do dia corrente, essa funcionalidade atende a agendeamentos com no mínimo 2 horas adiantado a partir da hora atual!') + + return - if(((new Date(timerPicker).getHours() === new Date().getHours()) && - (new Date(timerPicker).getMinutes() <= new Date().getMinutes())) || - (new Date(timerPicker).getHours() < new Date().getHours()) - ){ - alert('Para agendamentos do dia, é necessário que o horário do lembrete seja maior que o horário atual!') - return - } } - + props.func({ - 'scheduleId': scheduleId, - 'schedulingDate': `${startDate} ${timerPicker.getHours()}:${timerPicker.getMinutes()}:${timerPicker.getSeconds()}`, + 'scheduleId': scheduleId, + 'schedulingDate': `${startDate} ${currencyHourBefore}:00`, + 'schedulingTime': startDate+' '+formatedTimeHour(new Date(`${startDate} ${timerPicker.getHours()}:${timerPicker.getMinutes()}:00`)), 'message': textArea1 - }); + }); + setOpen(false); }; @@ -168,28 +262,101 @@ const textFieldSelect = (data) => { } +const handleChangeHourBefore = (event) => { -// Get from child 4 -// const textArea1Value = (data) => { -// console.log('textArea1Value: ',(data)); -// setTextArea1(data) -// } + console.log('textFihandleChangeHourBefore: ',event.target.value); + + setCurrency(event.target.value); + +}; + + useEffect(()=>{ - if (parseInt(timerPicker.getHours()) > 12 && parseInt(timerPicker.getHours()) < 18){ - setTextArea1('Boa tarde, '+greetMessageSchedule( formatedTimeHour(addHours(new Date(timerPicker), 1)))) - } - else if(parseInt(timerPicker.getHours()) < 12){ - setTextArea1('Bom dia, '+greetMessageSchedule( formatedTimeHour(addHours(new Date(timerPicker), 1)))) - } - else if(parseInt(timerPicker.getHours()) > 18){ + const hoursBeforeAvalible = (timer) =>{ + + let hours = [] + let hour = 1 + + if(startDate === dateCurrentFormated()){ + + console.log('HOJE++++') + + while(subHours(timer, hour).getHours()>=6 && + subHours(timer, hour).getHours()>=new Date().getHours() && + subHours(timer, hour).getHours()<=19){ + + console.log('******** TIMER: ', formatedTimeHour(subHours(timer,hour))) + + hours.push( + {value: formatedTimeHour(subHours(timer,hour)), + label: `${hour} HORA ANTES DO HORÁRIO DO AGENDAMENTO`}) + + hour++; + } + + if(hours.length>1){ + console.log('entrou----------------------: ', hours.length) + hours.pop() + setCurrency(hours[0].value) + } + else{ + setCurrency(null) + } + + } + else{ + + while(subHours(timer, hour).getHours()>=6 && subHours(timer, hour).getHours()<=19){ + + console.log('******** another day TIMER: ', formatedTimeHour(subHours(timer,hour))) + + hours.push( + {value: formatedTimeHour(subHours(timer,hour)), + label: `${hour} HORA ANTES DO HORÁRIO DO AGENDAMENTO`}) + + hour++; + } + + if(hours.length>0){ + console.log('entrou----------------------: ', hours.length) + setCurrency(hours[0].value) + } + else{ + setCurrency(null) + } + + } - setTextArea1('Boa noite, '+greetMessageSchedule( formatedTimeHour(addHours(new Date(timerPicker), 1)))) - } + return {time: hours, hour:hour} + + } + + setCurrenciesTimeBefore(hoursBeforeAvalible(timerPicker).time) -},[timerPicker]) +},[timerPicker, startDate]) + + +useEffect(()=>{ + + console.log('CURRENCY HOUR BEFORE: ', `${startDate} ${currencyHourBefore}:00`) + + let auxDate = new Date(`${startDate} ${currencyHourBefore}:00`) + + if (parseInt(auxDate.getHours()) > 11 && parseInt(auxDate.getHours()) < 18){ + + setTextArea1('Boa tarde, '+greetMessageSchedule( formatedTimeHour(new Date(timerPicker), 1))) + } + else if(parseInt(auxDate.getHours()) < 12){ + setTextArea1('Bom dia, '+greetMessageSchedule( formatedTimeHour(new Date(timerPicker), 1))) + } + else if(parseInt(auxDate.getHours()) > 17){ + setTextArea1('Boa noite, '+greetMessageSchedule( formatedTimeHour(new Date(timerPicker), 1))) + } + +},[currencyHourBefore, startDate, timerPicker]) const handleChange = (event) => { @@ -197,103 +364,185 @@ const handleChange = (event) => { setTextArea1(event.target.value); }; - - return ( + + + return ( - + scroll={scroll} + aria-labelledby="scroll-dialog-title" + aria-describedby="scroll-dialog-description" + > - {props.modal_header} - + {props.modal_header} + - - - + + + - - + + - - - Selecione uma opção para encerrar o Atendimento + + + Selecione uma opção para encerrar o Atendimento + + { + return {'value': obj.id, 'label': obj.name} + })}/> + - - { - return {'value': obj.id, 'label': obj.name} - })}/> - + - + + + {scheduleId==='2' && + + - - - {scheduleId==='2' && - - + Lembrete de retorno - Lembrete de retorno - - - - - - - - + + + + + + + - - - {/* */} + + + {currencyHourBefore && + + + {currenciesTimeBefore.map((option) => ( + + {option.label} + + ))} + + + } + + - + - - } - - - - - - -
- -
- -
-
- + + } + + {schedulesContact.length>0 && + + + + + + handleDeleteSchedule(selectedSchedule.id)} + > + Deseja realmente deletar esse Agendamento? + + Agendamentos + + + + + + + Data + + + Hora + + + Deletar + + + + + + <> + {schedulesContact.map((scheduleData, index) => ( + + {scheduleData.schedulingDate.split(' ')[0]} + {scheduleData.schedulingTime.split(' ')[1]} + + + { + setSelectedSchedule(scheduleData); + setConfirmModalOpen(true); + + }} + > + + + + + + ))} + + +
+
+
} + + + + + + + +
+ +
+ +
+ ); } diff --git a/frontend/src/components/Report/DatePicker/index.js b/frontend/src/components/Report/DatePicker/index.js index ee7e2ee..7f0597c 100644 --- a/frontend/src/components/Report/DatePicker/index.js +++ b/frontend/src/components/Report/DatePicker/index.js @@ -8,6 +8,9 @@ import { KeyboardDatePicker, MuiPickersUtilsProvider, } from '@material-ui/pickers'; + + + import ptBrLocale from "date-fns/locale/pt-BR"; diff --git a/frontend/src/components/Report/SelectField/index.js b/frontend/src/components/Report/SelectField/index.js index 0993b4d..5dd4cdc 100644 --- a/frontend/src/components/Report/SelectField/index.js +++ b/frontend/src/components/Report/SelectField/index.js @@ -15,7 +15,7 @@ const SelectTextFields = (props) => { useEffect(()=>{ - + props.func(currency); },[currency, props]) diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index 8f74035..fe0dd8f 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -83,7 +83,7 @@ const Ticket = () => { const [contact, setContact] = useState({}); const [ticket, setTicket] = useState({}); - const [schedule, setSchedule] = useState({}) + const [schedule, setSchedule] = useState({}) useEffect(() => { setLoading(true); @@ -96,12 +96,12 @@ const Ticket = () => { const { data } = await api.get("/tickets/" + ticketId); // setContact(data.contact); - // setTicket(data); + // setTicket(data); setContact(data.contact.contact); setTicket(data.contact); - setSchedule(data.schedules) + setSchedule(data.schedules) setLoading(false); } catch (err) { diff --git a/frontend/src/components/TicketActionButtons/index.js b/frontend/src/components/TicketActionButtons/index.js index c30c3ba..d7b65ae 100644 --- a/frontend/src/components/TicketActionButtons/index.js +++ b/frontend/src/components/TicketActionButtons/index.js @@ -65,7 +65,8 @@ const TicketActionButtons = ({ ticket, schedule }) => { render() };