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,28 +28,34 @@ interface Request {
contactId: number;
status: string;
userId: number;
msg?: string
msg?: string,
queueId?: string | undefined
}
const CreateTicketService = async ({
contactId,
status,
userId,
msg = ''
msg = '',
queueId = undefined
}: Request): Promise<Ticket> => {
try {
console.log('Create contact service........')
const defaultWhatsapp = await GetDefaultWhatsApp(userId);
const defaultWhatsapp = await GetDefaultWhatsApp(userId);
const user = await User.findByPk(userId, { raw: true, })
const user = await User.findByPk(userId, { raw: true, })
const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp, user?.profile);
if (!queueId) {
const matchingQueue = await whatsappQueueMatchingUserQueue(userId, defaultWhatsapp, user?.profile);
queueId = matchingQueue ? matchingQueue.queueId : undefined
}
const queueId = matchingQueue ? matchingQueue.queueId : undefined
await CheckContactOpenTickets(contactId, defaultWhatsapp.id);
const { isGroup } = await ShowContactService(contactId);
@ -77,7 +83,7 @@ const CreateTicketService = async ({
sendWhatsAppMessageSocket(ticket, msg)
}, 3000)
}

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,
[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", `%ADRIANO ROB%`)
};
// whereCondition = {
// ...whereCondition,
// "$message.body$": where(
// fn("LOWER", col("body")),
// "LIKE",
// `%${sanitizedSearchParam}%`
// )
// };
}
if (date) {
@ -200,8 +228,6 @@ const ListTicketsService = async ({
const offset = limit * (+pageNumber - 1);
const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition,
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

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

View File

@ -1,14 +1,18 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { IconButton } from "@mui/material";
import Paper from "@material-ui/core/Paper";
import SearchIcon from "@material-ui/icons/Search";
import InputBase from "@material-ui/core/InputBase";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Badge from "@material-ui/core/Badge";
import SearchIcon from "@material-ui/icons/Search";
import MoveToInboxIcon from "@material-ui/icons/MoveToInbox";
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 Switch from "@material-ui/core/Switch";
@ -63,6 +67,25 @@ const useStyles = makeStyles((theme) => ({
},
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,
background: "#fff",
display: "flex",
@ -78,6 +101,11 @@ const useStyles = makeStyles((theme) => ({
alignSelf: "center",
},
menuSearch: {
color: "grey",
alignSelf: "center",
},
searchInput: {
flex: 1,
border: "none",
@ -95,20 +123,21 @@ const useStyles = makeStyles((theme) => ({
},
}));
const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" }
const TicketsManager = () => {
const { tabOption, setTabOption } = useContext(TabTicketContext);
const {setSearchTicket} = useContext(SearchTicketContext)
const { setSearchTicket } = useContext(SearchTicketContext)
const classes = useStyles();
const [searchParam, setSearchParam] = useState("");
const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM);
const [tab, setTab] = useState("open");
const [tabOpen, setTabOpen] = useState("open");
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
const [showAllTickets, setShowAllTickets] = useState(false);
const searchInputRef = useRef();
const { user } = useContext(AuthContext);
const [openCount, setOpenCount] = useState(0);
@ -117,7 +146,11 @@ const TicketsManager = () => {
const userQueueIds = user.queues.map((q) => q.id);
const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []);
const [showContentSearch, setShowContentSearch] = useState(false)
const searchInputRef = useRef();
const searchContentInputRef = useRef();
const [inputSearch, setInputSearch] = useState('');
const [inputContentSearch, setInputContentSearch] = useState("")
useEffect(() => {
if (user.profile.toUpperCase() === "ADMIN") {
@ -141,7 +174,7 @@ const TicketsManager = () => {
if (tabOption === 'open') {
setTabOption('')
setSearchParam('');
setSearchParam(DEFAULT_SEARCH_PARAM);
setInputSearch('');
setTab("open");
return;
@ -163,12 +196,12 @@ const TicketsManager = () => {
setInputSearch(removeExtraSpace(searchedTerm))
setSearchTicket(searchParam)
setSearchTicket(searchParam.searchParam)
clearTimeout(searchTimeout);
if (searchedTerm === "") {
setSearchParam(searchedTerm);
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }))
setInputSearch(searchedTerm)
setTab("open");
return;
@ -176,11 +209,16 @@ const TicketsManager = () => {
searchTimeout = setTimeout(() => {
setSearchParam(searchedTerm);
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }));
}, 500);
};
const handleContentSearch = e => {
let searchedContentText = e.target.value.toLowerCase()
setInputContentSearch(searchedContentText)
}
const handleChangeTab = (e, newValue) => {
setTab(newValue);
};
@ -233,15 +271,34 @@ const TicketsManager = () => {
<Paper square elevation={0} className={classes.ticketOptionsBox}>
{tab === "search" ? (
<div className={classes.serachInputWrapper}>
<SearchIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchInputRef}
placeholder={i18n.t("tickets.search.placeholder")}
type="search"
value={inputSearch}
onChange={handleSearch}
/>
<div className={classes.searchInputHeader}>
<SearchIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchInputRef}
placeholder={i18n.t("tickets.search.placeholder")}
type="search"
value={inputSearch}
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>
) : (
<>
@ -340,17 +397,17 @@ const TicketsManager = () => {
</TabPanel>
<TabPanel value={tab} name="search" className={classes.ticketsWrapper}>
<TicketsList
searchParam={searchParam}
tab={tab}
showAll={true}
selectedQueueIds={selectedQueueIds}
/>
<TicketsList
searchParam={searchParam.searchParam}
tab={tab}
showAll={true}
selectedQueueIds={selectedQueueIds}
/>
</TabPanel>
</Paper>
);
};
export default TicketsManager;
export default TicketsManager;

View File

@ -37,8 +37,9 @@ import { Can } from "../../components/Can";
import apiBroker from "../../services/apiBroker";
import fileDownload from 'js-file-download'
import ContactCreateTicketModal from "../../components/ContactCreateTicketModal";
const reducer = (state, action) => {
@ -111,6 +112,7 @@ const Contacts = () => {
const [contacts, dispatch] = useReducer(reducer, []);
const [selectedContactId, setSelectedContactId] = useState(null);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [isCreateTicketModalOpen, setIsCreateTicketModalOpen] = useState(false)
const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [hasMore, setHasMore] = useState(false);
@ -118,17 +120,17 @@ const Contacts = () => {
const [onQueueStatus, setOnQueueProcessStatus] = useState(undefined)
const [zipfile, setZipFile] = useState()
const [zipfile, setZipFile] = useState()
async function handleChange(event) {
try {
if (event.target.files[0].size > 1024 * 1024 * 4){
if (event.target.files[0].size > 1024 * 1024 * 4) {
alert('Arquivo não pode ser maior que 4 MB!')
return
}
}
const formData = new FormData();
formData.append("adminId", user.id);
@ -302,6 +304,15 @@ const Contacts = () => {
setContactModalOpen(false);
};
const handleOpenCreateTicketModal = (contactId) => {
setSelectedContactId(contactId)
setIsCreateTicketModalOpen(true)
}
const handleCloseCreateTicketModal = () => {
setIsCreateTicketModalOpen(false)
}
const handleSaveTicket = async (contactId) => {
if (!contactId) return;
setLoading(true);
@ -415,21 +426,21 @@ const Contacts = () => {
switch (param) {
case 'empty':
return (
<>
<>
<input
type="file"
accept=".csv"
style={{ display: 'none' }}
onChange={handleChange}
id="contained-button-file"
/>
<input
type="file"
accept=".csv"
style={{ display: 'none' }}
onChange={handleChange}
id="contained-button-file"
/>
<label htmlFor="contained-button-file">
<Button variant="contained" color="primary" component="span">
CSV UPLOAD
</Button>
</label>
<label htmlFor="contained-button-file">
<Button variant="contained" color="primary" component="span">
CSV UPLOAD
</Button>
</label>
{/* <Button
disabled={query && query.length > 0 ? false : true}
@ -495,6 +506,11 @@ const Contacts = () => {
aria-labelledby="form-dialog-title"
contactId={selectedContactId}
></ContactModal>
<ContactCreateTicketModal
modalOpen={isCreateTicketModalOpen}
onClose={handleCloseCreateTicketModal}
contactId={selectedContactId}
/>
<ConfirmationModal
title={
deletingContact
@ -609,7 +625,7 @@ const Contacts = () => {
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleSaveTicket(contact.id)}
onClick={() => handleOpenCreateTicketModal(contact.id)}
>
<WhatsAppIcon />
</IconButton>
@ -646,4 +662,4 @@ const Contacts = () => {
);
};
export default Contacts;
export default Contacts;