Em andamento. Ajustes no frontend para que o atendende possa escolher fila. Alteração na busca por ticket pelo conteudo.

adriano 2023-06-26 14:46:29 -03:00
parent 34a0f72ac6
commit 4b23aad804
6 changed files with 283 additions and 58 deletions

View File

@ -28,14 +28,16 @@ interface Request {
contactId: number; contactId: number;
status: string; status: string;
userId: number; userId: number;
msg?: string msg?: string,
queueId?: string | undefined
} }
const CreateTicketService = async ({ const CreateTicketService = async ({
contactId, contactId,
status, status,
userId, userId,
msg = '' msg = '',
queueId = undefined
}: Request): Promise<Ticket> => { }: Request): Promise<Ticket> => {
try { try {
@ -46,9 +48,13 @@ const CreateTicketService = async ({
const user = await User.findByPk(userId, { raw: true, }) const user = await User.findByPk(userId, { raw: true, })
if (!queueId) {
const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp, user?.profile); const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp, user?.profile);
const queueId = matchingQueue ? matchingQueue.queueId : undefined queueId = matchingQueue ? matchingQueue.queueId : undefined
}
await CheckContactOpenTickets(contactId, defaultWhatsapp.id); await CheckContactOpenTickets(contactId, defaultWhatsapp.id);

View File

@ -161,6 +161,21 @@ const ListTicketsService = async ({
// } // }
// ]; // ];
includeCondition = [
...includeCondition,
{
model: Message,
as: "messages",
attributes: ["id", "body"],
where: {
body: where(fn("LOWER", col("body")), "LIKE", `%ADRIANO ROB%`)
},
required: false,
duplicating: false
}
];
whereCondition = { whereCondition = {
...whereCondition, ...whereCondition,
[Op.or]: [ [Op.or]: [
@ -173,8 +188,21 @@ const ListTicketsService = async ({
// { // {
// "$message.body$": where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParam}%`) // "$message.body$": where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParam}%`)
// } // }
] ],
"$message.body$": where(fn("LOWER", col("body")), "LIKE", `%ADRIANO ROB%`)
}; };
// whereCondition = {
// ...whereCondition,
// "$message.body$": where(
// fn("LOWER", col("body")),
// "LIKE",
// `%${sanitizedSearchParam}%`
// )
// };
} }
if (date) { if (date) {
@ -200,8 +228,6 @@ const ListTicketsService = async ({
const offset = limit * (+pageNumber - 1); const offset = limit * (+pageNumber - 1);
const { count, rows: tickets } = await Ticket.findAndCountAll({ const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition, where: whereCondition,
include: includeCondition, include: includeCondition,

View File

@ -0,0 +1,119 @@
import React, { useState, useEffect, useContext } from "react";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Select from "@material-ui/core/Select";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import { makeStyles } from "@material-ui/core";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { i18n } from "../../translate/i18n";
import ButtonWithSpinner from "../ButtonWithSpinner";
import { AuthContext } from "../../context/Auth/AuthContext";
import toastError from "../../errors/toastError";
import api from "../../services/api";
const useStyles = makeStyles((theme) => ({
maxWidth: {
width: "100%",
},
paper: {
minWidth: "300px"
}
}));
const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => {
const { user } = useContext(AuthContext);
const history = useHistory();
const [queues, setQueues] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedQueue, setSelectedQueue] = useState('');
const classes = useStyles();
useEffect(() => {
const userQueues = user.queues.map(({ id, name, color }) => { return { id, name, color } })
setQueues(userQueues)
}, [user]);
const handleClose = () => {
onClose();
};
const handleSaveTicket = async (e) => {
e.preventDefault()
if (!contactId) return;
if (!selectedQueue) {
toast.warning("Nenhuma Fila Selecionada")
return
}
setLoading(true);
try {
const { data: ticket } = await api.post("/tickets", {
contactId: contactId,
userId: user?.id,
queueId: selectedQueue,
status: "open",
});
history.push(`/tickets/${ticket.id}`);
} catch (err) {
toastError(err);
onClose()
}
setLoading(false);
};
return (
<Dialog open={modalOpen} onClose={handleClose} maxWidth="xs" scroll="paper" classes={{ paper: classes.paper }}>
<form onSubmit={handleSaveTicket}>
<DialogTitle id="form-dialog-title">
{i18n.t("newTicketModal.title")}
</DialogTitle>
<DialogContent dividers>
<FormControl variant="outlined" className={classes.maxWidth}>
<InputLabel>{i18n.t("Selecionar Fila")}</InputLabel>
<Select
value={selectedQueue}
onChange={(e) => setSelectedQueue(e.target.value)}
label={i18n.t("Filas")}
>
<MenuItem value={''}>&nbsp;</MenuItem>
{queues.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name[0].toUpperCase() + name.slice(1).toLowerCase()}</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={loading}
variant="outlined"
>
{i18n.t("newTicketModal.buttons.cancel")}
</Button>
<ButtonWithSpinner
variant="contained"
type="submit"
color="primary"
loading={loading}
>
{i18n.t("newTicketModal.buttons.ok")}
</ButtonWithSpinner>
</DialogActions>
</form>
</Dialog>
);
};
export default ContactCreateTicketModal;

View File

@ -202,6 +202,7 @@ const TicketsList = (props) => {
}); });
useEffect(() => { useEffect(() => {
if (!status && !searchParam) return; if (!status && !searchParam) return;
// if (searchParam) { // if (searchParam) {

View File

@ -1,14 +1,18 @@
import React, { useContext, useEffect, useRef, useState } from "react"; import React, { useContext, useEffect, useRef, useState } from "react";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { IconButton } from "@mui/material";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import SearchIcon from "@material-ui/icons/Search";
import InputBase from "@material-ui/core/InputBase"; import InputBase from "@material-ui/core/InputBase";
import Tabs from "@material-ui/core/Tabs"; import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab"; import Tab from "@material-ui/core/Tab";
import Badge from "@material-ui/core/Badge"; import Badge from "@material-ui/core/Badge";
import SearchIcon from "@material-ui/icons/Search";
import MoveToInboxIcon from "@material-ui/icons/MoveToInbox"; import MoveToInboxIcon from "@material-ui/icons/MoveToInbox";
import CheckBoxIcon from "@material-ui/icons/CheckBox"; import CheckBoxIcon from "@material-ui/icons/CheckBox";
import MenuIcon from "@material-ui/icons/Menu";
import FindInPageIcon from '@material-ui/icons/FindInPage';
import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch"; import Switch from "@material-ui/core/Switch";
@ -63,6 +67,25 @@ const useStyles = makeStyles((theme) => ({
}, },
serachInputWrapper: { serachInputWrapper: {
flex: 1,
display: "flex",
flexDirection: "column",
gap: "10px",
borderRadius: 40,
padding: 4,
marginRight: theme.spacing(1),
},
searchInputHeader: {
flex: 1,
background: "#fff",
display: "flex",
borderRadius: 40,
padding: 4,
marginRight: theme.spacing(1),
},
searchContentInput: {
flex: 1, flex: 1,
background: "#fff", background: "#fff",
display: "flex", display: "flex",
@ -78,6 +101,11 @@ const useStyles = makeStyles((theme) => ({
alignSelf: "center", alignSelf: "center",
}, },
menuSearch: {
color: "grey",
alignSelf: "center",
},
searchInput: { searchInput: {
flex: 1, flex: 1,
border: "none", border: "none",
@ -95,6 +123,8 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" }
const TicketsManager = () => { const TicketsManager = () => {
const { tabOption, setTabOption } = useContext(TabTicketContext); const { tabOption, setTabOption } = useContext(TabTicketContext);
@ -103,12 +133,11 @@ const TicketsManager = () => {
const classes = useStyles(); const classes = useStyles();
const [searchParam, setSearchParam] = useState(""); const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM);
const [tab, setTab] = useState("open"); const [tab, setTab] = useState("open");
const [tabOpen, setTabOpen] = useState("open"); const [tabOpen, setTabOpen] = useState("open");
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
const [showAllTickets, setShowAllTickets] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false);
const searchInputRef = useRef();
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext);
const [openCount, setOpenCount] = useState(0); const [openCount, setOpenCount] = useState(0);
@ -117,7 +146,11 @@ const TicketsManager = () => {
const userQueueIds = user.queues.map((q) => q.id); const userQueueIds = user.queues.map((q) => q.id);
const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []); const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []);
const [showContentSearch, setShowContentSearch] = useState(false)
const searchInputRef = useRef();
const searchContentInputRef = useRef();
const [inputSearch, setInputSearch] = useState(''); const [inputSearch, setInputSearch] = useState('');
const [inputContentSearch, setInputContentSearch] = useState("")
useEffect(() => { useEffect(() => {
if (user.profile.toUpperCase() === "ADMIN") { if (user.profile.toUpperCase() === "ADMIN") {
@ -141,7 +174,7 @@ const TicketsManager = () => {
if (tabOption === 'open') { if (tabOption === 'open') {
setTabOption('') setTabOption('')
setSearchParam(''); setSearchParam(DEFAULT_SEARCH_PARAM);
setInputSearch(''); setInputSearch('');
setTab("open"); setTab("open");
return; return;
@ -163,12 +196,12 @@ const TicketsManager = () => {
setInputSearch(removeExtraSpace(searchedTerm)) setInputSearch(removeExtraSpace(searchedTerm))
setSearchTicket(searchParam) setSearchTicket(searchParam.searchParam)
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
if (searchedTerm === "") { if (searchedTerm === "") {
setSearchParam(searchedTerm); setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }))
setInputSearch(searchedTerm) setInputSearch(searchedTerm)
setTab("open"); setTab("open");
return; return;
@ -176,11 +209,16 @@ const TicketsManager = () => {
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
setSearchParam(searchedTerm); setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }));
}, 500); }, 500);
}; };
const handleContentSearch = e => {
let searchedContentText = e.target.value.toLowerCase()
setInputContentSearch(searchedContentText)
}
const handleChangeTab = (e, newValue) => { const handleChangeTab = (e, newValue) => {
setTab(newValue); setTab(newValue);
}; };
@ -233,6 +271,7 @@ const TicketsManager = () => {
<Paper square elevation={0} className={classes.ticketOptionsBox}> <Paper square elevation={0} className={classes.ticketOptionsBox}>
{tab === "search" ? ( {tab === "search" ? (
<div className={classes.serachInputWrapper}> <div className={classes.serachInputWrapper}>
<div className={classes.searchInputHeader}>
<SearchIcon className={classes.searchIcon} /> <SearchIcon className={classes.searchIcon} />
<InputBase <InputBase
className={classes.searchInput} className={classes.searchInput}
@ -242,6 +281,24 @@ const TicketsManager = () => {
value={inputSearch} value={inputSearch}
onChange={handleSearch} onChange={handleSearch}
/> />
<IconButton onClick={() => setShowContentSearch(prev => !prev)}>
<MenuIcon className={classes.menuSearch} />
</IconButton>
</div>
{
showContentSearch ?
(<div className={classes.searchContentInput}>
<FindInPageIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchContentInputRef}
placeholder={i18n.t("Busca por conteúdo")}
type="search"
value={inputContentSearch}
onChange={handleContentSearch}
/>
</div>) : null
}
</div> </div>
) : ( ) : (
<> <>
@ -342,7 +399,7 @@ const TicketsManager = () => {
<TicketsList <TicketsList
searchParam={searchParam} searchParam={searchParam.searchParam}
tab={tab} tab={tab}
showAll={true} showAll={true}
selectedQueueIds={selectedQueueIds} selectedQueueIds={selectedQueueIds}

View File

@ -37,6 +37,7 @@ import { Can } from "../../components/Can";
import apiBroker from "../../services/apiBroker"; import apiBroker from "../../services/apiBroker";
import fileDownload from 'js-file-download' import fileDownload from 'js-file-download'
import ContactCreateTicketModal from "../../components/ContactCreateTicketModal";
@ -111,6 +112,7 @@ const Contacts = () => {
const [contacts, dispatch] = useReducer(reducer, []); const [contacts, dispatch] = useReducer(reducer, []);
const [selectedContactId, setSelectedContactId] = useState(null); const [selectedContactId, setSelectedContactId] = useState(null);
const [contactModalOpen, setContactModalOpen] = useState(false); const [contactModalOpen, setContactModalOpen] = useState(false);
const [isCreateTicketModalOpen, setIsCreateTicketModalOpen] = useState(false)
const [deletingContact, setDeletingContact] = useState(null); const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
@ -302,6 +304,15 @@ const Contacts = () => {
setContactModalOpen(false); setContactModalOpen(false);
}; };
const handleOpenCreateTicketModal = (contactId) => {
setSelectedContactId(contactId)
setIsCreateTicketModalOpen(true)
}
const handleCloseCreateTicketModal = () => {
setIsCreateTicketModalOpen(false)
}
const handleSaveTicket = async (contactId) => { const handleSaveTicket = async (contactId) => {
if (!contactId) return; if (!contactId) return;
setLoading(true); setLoading(true);
@ -495,6 +506,11 @@ const Contacts = () => {
aria-labelledby="form-dialog-title" aria-labelledby="form-dialog-title"
contactId={selectedContactId} contactId={selectedContactId}
></ContactModal> ></ContactModal>
<ContactCreateTicketModal
modalOpen={isCreateTicketModalOpen}
onClose={handleCloseCreateTicketModal}
contactId={selectedContactId}
/>
<ConfirmationModal <ConfirmationModal
title={ title={
deletingContact deletingContact
@ -609,7 +625,7 @@ const Contacts = () => {
<TableCell align="center"> <TableCell align="center">
<IconButton <IconButton
size="small" size="small"
onClick={() => handleSaveTicket(contact.id)} onClick={() => handleOpenCreateTicketModal(contact.id)}
> >
<WhatsAppIcon /> <WhatsAppIcon />
</IconButton> </IconButton>