diff --git a/backend/src/controllers/ApiController.ts b/backend/src/controllers/ApiController.ts index 33fb7df..66d72f1 100644 --- a/backend/src/controllers/ApiController.ts +++ b/backend/src/controllers/ApiController.ts @@ -4,6 +4,7 @@ import AppError from "../errors/AppError"; import GetDefaultWhatsApp from "../helpers/GetDefaultWhatsApp"; import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead"; import Message from "../models/Message"; +import Whatsapp from "../models/Whatsapp"; import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService"; import FindOrCreateTicketService from "../services/TicketServices/FindOrCreateTicketService"; import ShowTicketService from "../services/TicketServices/ShowTicketService"; @@ -13,6 +14,10 @@ import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia"; import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage"; +type WhatsappData = { + whatsappId: number; +} + type MessageData = { body: string; fromMe: boolean; @@ -24,7 +29,10 @@ interface ContactData { number: string; } -const createContact = async (newContact: string) => { +const createContact = async ( + whatsappId: number | undefined, + newContact: string +) => { await CheckIsValidContact(newContact); const validNumber: any = await CheckContactNumber(newContact); @@ -42,11 +50,21 @@ const createContact = async (newContact: string) => { const contact = await CreateOrUpdateContactService(contactData); - const defaultWhatsapp = await GetDefaultWhatsApp(); + let whatsapp:Whatsapp | null; + + if(whatsappId === undefined) { + whatsapp = await GetDefaultWhatsApp(); + } else { + whatsapp = await Whatsapp.findByPk(whatsappId); + + if(whatsapp === null) { + throw new AppError(`whatsapp #${whatsappId} not found`); + } + } const createTicket = await FindOrCreateTicketService( contact, - defaultWhatsapp.id, + whatsapp.id, 1 ); @@ -59,6 +77,7 @@ const createContact = async (newContact: string) => { export const index = async (req: Request, res: Response): Promise => { const newContact: ContactData = req.body; + const { whatsappId }: WhatsappData = req.body; const { body, quotedMsg }: MessageData = req.body; const medias = req.files as Express.Multer.File[]; @@ -76,7 +95,7 @@ export const index = async (req: Request, res: Response): Promise => { throw new AppError(err.message); } - const contactAndTicket = await createContact(newContact.number); + const contactAndTicket = await createContact(whatsappId, newContact.number); if (medias) { await Promise.all( diff --git a/backend/src/controllers/ImportPhoneContactsController.ts b/backend/src/controllers/ImportPhoneContactsController.ts index 01f1cfc..2b19890 100644 --- a/backend/src/controllers/ImportPhoneContactsController.ts +++ b/backend/src/controllers/ImportPhoneContactsController.ts @@ -2,7 +2,8 @@ import { Request, Response } from "express"; import ImportContactsService from "../services/WbotServices/ImportContactsService"; export const store = async (req: Request, res: Response): Promise => { - await ImportContactsService(); + const userId:number = parseInt(req.user.id); + await ImportContactsService(userId); return res.status(200).json({ message: "contacts imported" }); }; diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index 06d329d..001d97b 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -27,7 +27,7 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { email, password, name, profile, queueIds } = req.body; + const { email, password, name, profile, queueIds, whatsappId } = req.body; if ( req.url === "/signup" && @@ -43,7 +43,8 @@ export const store = async (req: Request, res: Response): Promise => { password, name, profile, - queueIds + queueIds, + whatsappId }); const io = getIO(); diff --git a/backend/src/database/migrations/20220223095932-add-whatsapp-to-user.ts b/backend/src/database/migrations/20220223095932-add-whatsapp-to-user.ts new file mode 100644 index 0000000..fc53178 --- /dev/null +++ b/backend/src/database/migrations/20220223095932-add-whatsapp-to-user.ts @@ -0,0 +1,17 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Users", "whatsappId", { + type: DataTypes.INTEGER, + references: { model: "Whatsapps", key: "id" }, + onUpdate: "CASCADE", + onDelete: "SET NULL", + allowNull: true + },); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Users", "whatsappId"); + } +}; diff --git a/backend/src/helpers/CheckContactOpenTickets.ts b/backend/src/helpers/CheckContactOpenTickets.ts index 3437cce..71de597 100644 --- a/backend/src/helpers/CheckContactOpenTickets.ts +++ b/backend/src/helpers/CheckContactOpenTickets.ts @@ -2,9 +2,12 @@ import { Op } from "sequelize"; import AppError from "../errors/AppError"; import Ticket from "../models/Ticket"; -const CheckContactOpenTickets = async (contactId: number): Promise => { +const CheckContactOpenTickets = async ( + contactId: number, + whatsappId: number +): Promise => { const ticket = await Ticket.findOne({ - where: { contactId, status: { [Op.or]: ["open", "pending"] } } + where: { contactId, whatsappId, status: { [Op.or]: ["open", "pending"] } } }); if (ticket) { diff --git a/backend/src/helpers/GetDefaultWhatsApp.ts b/backend/src/helpers/GetDefaultWhatsApp.ts index afcb362..ef74cda 100644 --- a/backend/src/helpers/GetDefaultWhatsApp.ts +++ b/backend/src/helpers/GetDefaultWhatsApp.ts @@ -1,7 +1,17 @@ import AppError from "../errors/AppError"; import Whatsapp from "../models/Whatsapp"; +import GetDefaultWhatsAppByUser from "./GetDefaultWhatsAppByUser"; + +const GetDefaultWhatsApp = async ( + userId?: number +): Promise => { + if(userId) { + const whatsappByUser = await GetDefaultWhatsAppByUser(userId); + if(whatsappByUser !== null) { + return whatsappByUser; + } + } -const GetDefaultWhatsApp = async (): Promise => { const defaultWhatsapp = await Whatsapp.findOne({ where: { isDefault: true } }); diff --git a/backend/src/helpers/GetDefaultWhatsAppByUser.ts b/backend/src/helpers/GetDefaultWhatsAppByUser.ts new file mode 100644 index 0000000..e651777 --- /dev/null +++ b/backend/src/helpers/GetDefaultWhatsAppByUser.ts @@ -0,0 +1,20 @@ +import User from "../models/User"; +import Whatsapp from "../models/Whatsapp"; +import { logger } from "../utils/logger"; + +const GetDefaultWhatsAppByUser = async ( + userId: number +): Promise => { + const user = await User.findByPk(userId, {include: ["whatsapp"]}); + if( user === null ) { + return null; + } + + if(user.whatsapp !== null) { + logger.info(`Found whatsapp linked to user '${user.name}' is '${user.whatsapp.name}'.`); + } + + return user.whatsapp; +}; + +export default GetDefaultWhatsAppByUser; diff --git a/backend/src/helpers/GetTicketWbot.ts b/backend/src/helpers/GetTicketWbot.ts index 0caa6b0..802e8e9 100644 --- a/backend/src/helpers/GetTicketWbot.ts +++ b/backend/src/helpers/GetTicketWbot.ts @@ -5,7 +5,7 @@ import Ticket from "../models/Ticket"; const GetTicketWbot = async (ticket: Ticket): Promise => { if (!ticket.whatsappId) { - const defaultWhatsapp = await GetDefaultWhatsApp(); + const defaultWhatsapp = await GetDefaultWhatsApp(ticket.user.id); await ticket.$set("whatsapp", defaultWhatsapp); } diff --git a/backend/src/helpers/SerializeUser.ts b/backend/src/helpers/SerializeUser.ts index 3802500..928f75b 100644 --- a/backend/src/helpers/SerializeUser.ts +++ b/backend/src/helpers/SerializeUser.ts @@ -1,5 +1,6 @@ import Queue from "../models/Queue"; import User from "../models/User"; +import Whatsapp from "../models/Whatsapp"; interface SerializedUser { id: number; @@ -7,6 +8,7 @@ interface SerializedUser { email: string; profile: string; queues: Queue[]; + whatsapp: Whatsapp; } export const SerializeUser = (user: User): SerializedUser => { @@ -15,6 +17,7 @@ export const SerializeUser = (user: User): SerializedUser => { name: user.name, email: user.email, profile: user.profile, - queues: user.queues + queues: user.queues, + whatsapp: user.whatsapp }; }; diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 9be664b..73edff4 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -11,12 +11,15 @@ import { AutoIncrement, Default, HasMany, - BelongsToMany + BelongsToMany, + ForeignKey, + BelongsTo } from "sequelize-typescript"; import { hash, compare } from "bcryptjs"; import Ticket from "./Ticket"; import Queue from "./Queue"; import UserQueue from "./UserQueue"; +import Whatsapp from "./Whatsapp"; @Table class User extends Model { @@ -45,6 +48,13 @@ class User extends Model { @Column profile: string; + @ForeignKey(() => Whatsapp) + @Column + whatsappId: number; + + @BelongsTo(() => Whatsapp) + whatsapp: Whatsapp; + @CreatedAt createdAt: Date; diff --git a/backend/src/services/TicketServices/CreateTicketService.ts b/backend/src/services/TicketServices/CreateTicketService.ts index 4ea16cf..bfda479 100644 --- a/backend/src/services/TicketServices/CreateTicketService.ts +++ b/backend/src/services/TicketServices/CreateTicketService.ts @@ -2,30 +2,39 @@ import AppError from "../../errors/AppError"; import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; import Ticket from "../../models/Ticket"; +import User from "../../models/User"; import ShowContactService from "../ContactServices/ShowContactService"; interface Request { contactId: number; status: string; userId: number; + queueId ?: number; } const CreateTicketService = async ({ contactId, status, - userId + userId, + queueId }: Request): Promise => { - const defaultWhatsapp = await GetDefaultWhatsApp(); + const defaultWhatsapp = await GetDefaultWhatsApp(userId); - await CheckContactOpenTickets(contactId); + await CheckContactOpenTickets(contactId, defaultWhatsapp.id); const { isGroup } = await ShowContactService(contactId); + if(queueId === undefined) { + const user = await User.findByPk(userId, { include: ["queues"]}); + queueId = user?.queues.length === 1 ? user.queues[0].id : undefined; + } + const { id }: Ticket = await defaultWhatsapp.$create("ticket", { contactId, status, isGroup, - userId + userId, + queueId }); const ticket = await Ticket.findByPk(id, { include: ["contact"] }); diff --git a/backend/src/services/TicketServices/FindOrCreateTicketService.ts b/backend/src/services/TicketServices/FindOrCreateTicketService.ts index bf4c2b0..702f59c 100644 --- a/backend/src/services/TicketServices/FindOrCreateTicketService.ts +++ b/backend/src/services/TicketServices/FindOrCreateTicketService.ts @@ -15,7 +15,8 @@ const FindOrCreateTicketService = async ( status: { [Op.or]: ["open", "pending"] }, - contactId: groupContact ? groupContact.id : contact.id + contactId: groupContact ? groupContact.id : contact.id, + whatsappId: whatsappId } }); @@ -26,7 +27,8 @@ const FindOrCreateTicketService = async ( if (!ticket && groupContact) { ticket = await Ticket.findOne({ where: { - contactId: groupContact.id + contactId: groupContact.id, + whatsappId: whatsappId }, order: [["updatedAt", "DESC"]] }); @@ -46,7 +48,8 @@ const FindOrCreateTicketService = async ( updatedAt: { [Op.between]: [+subHours(new Date(), 2), +new Date()] }, - contactId: contact.id + contactId: contact.id, + whatsappId: whatsappId }, order: [["updatedAt", "DESC"]] }); diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index 29e165c..b8d8dd6 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -6,6 +6,7 @@ import Contact from "../../models/Contact"; import Message from "../../models/Message"; import Queue from "../../models/Queue"; import ShowUserService from "../UserServices/ShowUserService"; +import Whatsapp from "../../models/Whatsapp"; interface Request { searchParam?: string; @@ -50,6 +51,11 @@ const ListTicketsService = async ({ model: Queue, as: "queue", attributes: ["id", "name", "color"] + }, + { + model: Whatsapp, + as: "whatsapp", + attributes: ["name"] } ]; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index 3efc5db..8cac249 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -10,6 +10,7 @@ interface TicketData { status?: string; userId?: number; queueId?: number; + whatsappId?: number; } interface Request { @@ -27,16 +28,20 @@ const UpdateTicketService = async ({ ticketData, ticketId }: Request): Promise => { - const { status, userId, queueId } = ticketData; + const { status, userId, queueId, whatsappId } = ticketData; const ticket = await ShowTicketService(ticketId); await SetTicketMessagesAsRead(ticket); + if(whatsappId && ticket.whatsappId !== whatsappId) { + await CheckContactOpenTickets(ticket.contactId, whatsappId); + } + const oldStatus = ticket.status; const oldUserId = ticket.user?.id; if (oldStatus === "closed") { - await CheckContactOpenTickets(ticket.contact.id); + await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); } await ticket.update({ @@ -46,6 +51,11 @@ const UpdateTicketService = async ({ }); + if(whatsappId) { + await ticket.update({ + whatsappId + }); + } await ticket.reload(); diff --git a/backend/src/services/UserServices/CreateUserService.ts b/backend/src/services/UserServices/CreateUserService.ts index 098846b..db1639b 100644 --- a/backend/src/services/UserServices/CreateUserService.ts +++ b/backend/src/services/UserServices/CreateUserService.ts @@ -10,6 +10,7 @@ interface Request { name: string; queueIds?: number[]; profile?: string; + whatsappId?: number; } interface Response { @@ -24,7 +25,8 @@ const CreateUserService = async ({ password, name, queueIds = [], - profile = "admin" + profile = "admin", + whatsappId }: Request): Promise => { const schema = Yup.object().shape({ name: Yup.string().required().min(2), @@ -56,18 +58,17 @@ const CreateUserService = async ({ email, password, name, - profile + profile, + whatsappId: whatsappId ? whatsappId : null }, - { include: ["queues"] } + { include: ["queues", "whatsapp"] } ); await user.$set("queues", queueIds); await user.reload(); - const serializedUser = SerializeUser(user); - - return serializedUser; + return SerializeUser(user); }; export default CreateUserService; diff --git a/backend/src/services/UserServices/ListUsersService.ts b/backend/src/services/UserServices/ListUsersService.ts index fb8a202..200ce05 100644 --- a/backend/src/services/UserServices/ListUsersService.ts +++ b/backend/src/services/UserServices/ListUsersService.ts @@ -1,6 +1,7 @@ import { Sequelize, Op } from "sequelize"; import Queue from "../../models/Queue"; import User from "../../models/User"; +import Whatsapp from "../../models/Whatsapp"; interface Request { searchParam?: string; @@ -39,7 +40,8 @@ const ListUsersService = async ({ offset, order: [["createdAt", "DESC"]], include: [ - { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + { model: Queue, as: "queues", attributes: ["id", "name", "color"] }, + { model: Whatsapp, as: "whatsapp", attributes: ["id", "name"] }, ] }); diff --git a/backend/src/services/UserServices/ShowUserService.ts b/backend/src/services/UserServices/ShowUserService.ts index 03ecab4..d98a8c0 100644 --- a/backend/src/services/UserServices/ShowUserService.ts +++ b/backend/src/services/UserServices/ShowUserService.ts @@ -1,12 +1,14 @@ import User from "../../models/User"; import AppError from "../../errors/AppError"; import Queue from "../../models/Queue"; +import Whatsapp from "../../models/Whatsapp"; const ShowUserService = async (id: string | number): Promise => { const user = await User.findByPk(id, { - attributes: ["name", "id", "email", "profile", "tokenVersion"], + attributes: ["name", "id", "email", "profile", "tokenVersion", "whatsappId"], include: [ - { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + { model: Queue, as: "queues", attributes: ["id", "name", "color"] }, + { model: Whatsapp, as: "whatsapp", attributes: ["id", "name"] }, ], order: [ [ { model: Queue, as: "queues"}, 'name', 'asc' ] ] }); diff --git a/backend/src/services/UserServices/UpdateUserService.ts b/backend/src/services/UserServices/UpdateUserService.ts index 114d557..44be3f4 100644 --- a/backend/src/services/UserServices/UpdateUserService.ts +++ b/backend/src/services/UserServices/UpdateUserService.ts @@ -1,6 +1,7 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; +import { SerializeUser } from "../../helpers/SerializeUser"; import ShowUserService from "./ShowUserService"; interface UserData { @@ -9,6 +10,7 @@ interface UserData { name?: string; profile?: string; queueIds?: number[]; + whatsappId?: number; } interface Request { @@ -36,7 +38,7 @@ const UpdateUserService = async ({ password: Yup.string() }); - const { email, password, profile, name, queueIds = [] } = userData; + const { email, password, profile, name, queueIds = [], whatsappId } = userData; try { await schema.validate({ email, password, profile, name }); @@ -48,22 +50,15 @@ const UpdateUserService = async ({ email, password, profile, - name + name, + whatsappId: whatsappId ? whatsappId : null }); await user.$set("queues", queueIds); await user.reload(); - const serializedUser = { - id: user.id, - name: user.name, - email: user.email, - profile: user.profile, - queues: user.queues - }; - - return serializedUser; + return SerializeUser(user); }; export default UpdateUserService; diff --git a/backend/src/services/WbotServices/ImportContactsService.ts b/backend/src/services/WbotServices/ImportContactsService.ts index ee6fd1f..531fff5 100644 --- a/backend/src/services/WbotServices/ImportContactsService.ts +++ b/backend/src/services/WbotServices/ImportContactsService.ts @@ -3,8 +3,8 @@ import { getWbot } from "../../libs/wbot"; import Contact from "../../models/Contact"; import { logger } from "../../utils/logger"; -const ImportContactsService = async (): Promise => { - const defaultWhatsapp = await GetDefaultWhatsApp(); +const ImportContactsService = async (userId:number): Promise => { + const defaultWhatsapp = await GetDefaultWhatsApp(userId); const wbot = getWbot(defaultWhatsapp.id); diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 8ddff74..d0fc01b 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -99,6 +99,21 @@ const useStyles = makeStyles(theme => ({ top: "0%", left: "0%", }, + + userTag: { + position: "absolute", + marginRight: 5, + right: 5, + bottom: 5, + background:"#2576D2", + color: "#ffffff", + border:"1px solid #CCC", + padding: 1, + paddingLeft: 5, + paddingRight: 5, + borderRadius: 10, + fontSize: "1em" + }, })); const TicketListItem = ({ ticket }) => { @@ -196,6 +211,9 @@ const TicketListItem = ({ ticket }) => { )} )} + {ticket.whatsappId && ( +
{ticket.whatsapp.name}
+ )} } secondary={ diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 16591a2..d7bcd56 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -94,6 +94,7 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { modalOpen={transferTicketModalOpen} onClose={handleCloseTransferTicketModal} ticketid={ticket.id} + ticketWhatsappId={ticket.whatsappId} /> ); diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index 67a0910..3fa2f03 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { useHistory } from "react-router-dom"; import Button from "@material-ui/core/Button"; @@ -23,6 +23,9 @@ import api from "../../services/api"; import ButtonWithSpinner from "../ButtonWithSpinner"; import toastError from "../../errors/toastError"; import useQueues from "../../hooks/useQueues"; +import useWhatsApps from "../../hooks/useWhatsApps"; +import { AuthContext } from "../../context/Auth/AuthContext"; +import { Can } from "../Can"; const useStyles = makeStyles((theme) => ({ maxWidth: { @@ -34,7 +37,7 @@ const filterOptions = createFilterOptions({ trim: true, }); -const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { +const TransferTicketModal = ({ modalOpen, onClose, ticketid, ticketWhatsappId }) => { const history = useHistory(); const [options, setOptions] = useState([]); const [queues, setQueues] = useState([]); @@ -43,8 +46,12 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { const [searchParam, setSearchParam] = useState(""); const [selectedUser, setSelectedUser] = useState(null); const [selectedQueue, setSelectedQueue] = useState(''); + const [selectedWhatsapp, setSelectedWhatsapp] = useState(ticketWhatsappId); const classes = useStyles(); const { findAll: findAllQueues } = useQueues(); + const { loadingWhatsapps, whatsApps } = useWhatsApps(); + + const { user: loggedInUser } = useContext(AuthContext); useEffect(() => { const loadQueues = async () => { @@ -107,6 +114,10 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { } } + if(selectedWhatsapp) { + data.whatsappId = selectedWhatsapp; + } + await api.put(`/tickets/${ticketid}`, data); setLoading(false); @@ -177,6 +188,24 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { ))} + (!loadingWhatsapps && + + {i18n.t("transferTicketModal.fieldConnectionLabel")} + + + )} + />