Em andamento. Ajustes no frontend para que o atendende possa escolher fila. Alteração na busca por ticket pelo conteudo.
parent
34a0f72ac6
commit
4b23aad804
|
@ -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)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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={''}> </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;
|
|
@ -201,7 +201,8 @@ const TicketsList = (props) => {
|
|||
tab
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
|
||||
if (!status && !searchParam) return;
|
||||
|
||||
// if (searchParam) {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue