Skip to content

Commit

Permalink
Exibindo as Tags e Escolha de Setor
Browse files Browse the repository at this point in the history
Exibindo as Tags na lista de tickets e tornando obrigatório a escolha de setor quando recebe um vcard e na página de contatos
  • Loading branch information
rtenorioh committed Apr 9, 2023
1 parent 0f0b988 commit f08c195
Show file tree
Hide file tree
Showing 11 changed files with 730 additions and 153 deletions.
26 changes: 17 additions & 9 deletions backend/src/helpers/CheckContactOpenTickets.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { Op } from "sequelize";
import AppError from "../errors/AppError";
import Ticket from "../models/Ticket";
import User from "../models/User";

const CheckContactOpenTickets = async (
contactId: number,
whatsappId: number
): Promise<void> => {
const ticket = await Ticket.findOne({
where: {
contactId,
whatsappId,
status: {
[Op.or]: ["open", "pending"]
}
}
where: {
contactId,
whatsappId,
status: {
[Op.or]: ["open", "pending"]
}
},
include: [{ model: User, as: "user" }]
});

if (ticket) {
throw new AppError("ERR_OTHER_OPEN_TICKET");
const userName = ticket.user?.name;

if (userName) {
throw new AppError(`ERR_OPEN_USER_TICKET ${userName}`);
} else {
throw new AppError("ERR_NONE_USER_TICKET");
}
}
};

export default CheckContactOpenTickets;
export default CheckContactOpenTickets;
27 changes: 27 additions & 0 deletions frontend/src/components/ContactTag/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { makeStyles } from "@material-ui/styles";

const useStyles = makeStyles(theme => ({
tag: {
padding: "1px 5px",
borderRadius: "3px",
fontSize: "0.8em",
fontWeight: "bold",
color: "#FFF",
marginRight: "5px",
marginBottom: "3px",
whiteSpace: "nowrap",
}
}));

const ContactTag = ({ tag }) => {
const classes = useStyles();

return (
<div className={classes.tag} style={{ backgroundColor: tag.color }}>
{tag.name.toUpperCase()}
</div>
)
}

export default ContactTag;
277 changes: 277 additions & 0 deletions frontend/src/components/NewTicketModalPageContact/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import React, { useState, useEffect, useContext } from "react";

import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
ListItemText,
MenuItem,
Select,
TextField
} from "@material-ui/core";

import Autocomplete, {
createFilterOptions,
} from "@material-ui/lab/Autocomplete";

import ButtonWithSpinner from "../ButtonWithSpinner";
import ContactModal from "../ContactModal";

import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import { toast } from "react-toastify";

const filter = createFilterOptions({
trim: true,
});

const NewTicketModalPageContact = ({ modalOpen, onClose, initialContact }) => {

const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [searchParam, setSearchParam] = useState("");
const [selectedContact, setSelectedContact] = useState(null);
const [selectedQueue, setSelectedQueue] = useState("");
const [newContact, setNewContact] = useState({});
const [contactModalOpen, setContactModalOpen] = useState(false);
const { user } = useContext(AuthContext);

useEffect(() => {
if (initialContact?.id !== undefined) {
setOptions([initialContact]);
setSelectedContact(initialContact);
}
}, [initialContact]);

useEffect(() => {
if (!modalOpen || searchParam.length < 3) {
setLoading(false);
return;
}
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
try {
const { data } = await api.get("contacts", {
params: { searchParam },
});
setOptions(data.contacts);
setLoading(false);
} catch (err) {
setLoading(false);
toastError(err);
}
};

fetchContacts();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, modalOpen]);

const handleClose = () => {
onClose();
setSearchParam("");
setSelectedContact(null);
};

const handleSaveTicket = async contactId => {
if (!contactId) return;
if (selectedQueue === "" && user.profile !== 'admin') {
toast.error(i18n.t("newTicketModalContactPage.queue"));
return;
}
setLoading(true);
try {
const queueId = selectedQueue !== "" ? selectedQueue : null;
const { data: ticket } = await api.post("/tickets", {
contactId: contactId,
queueId,
userId: user.id,
status: "open",
});
onClose(ticket);
} catch (err) {
toastError(err);
}
setLoading(false);
};

const handleSelectOption = (e, newValue) => {
if (newValue?.number) {
setSelectedContact(newValue);
} else if (newValue?.name) {
setNewContact({ name: newValue.name });
setContactModalOpen(true);
}
};

const handleCloseContactModal = () => {
setContactModalOpen(false);
};

const handleAddNewContactTicket = contact => {
handleSaveTicket(contact.id);
};

const createAddContactOption = (filterOptions, params) => {
const filtered = filter(filterOptions, params);

if (params.inputValue !== "" && !loading && searchParam.length >= 3) {
filtered.push({
name: `${params.inputValue}`,
});
}

return filtered;
};

const renderOption = option => {
if (option.number) {
return `${option.name} - ${option.number}`;
} else {
return `${i18n.t("newTicketModalContactPage.add")} ${option.name}`;
}
};

const renderOptionLabel = option => {
if (option.number) {
return `${option.name} - ${option.number}`;
} else {
return `${option.name}`;
}
};

const renderContactAutocomplete = () => {
if (initialContact === undefined || initialContact.id === undefined) {
return (
<Grid xs={12} item>
<Autocomplete
fullWidth
options={options}
loading={loading}
clearOnBlur
autoHighlight
freeSolo
clearOnEscape
getOptionLabel={renderOptionLabel}
renderOption={renderOption}
filterOptions={createAddContactOption}
onChange={(e, newValue) => handleSelectOption(e, newValue)}
renderInput={params => (
<TextField
{...params}
label={i18n.t("newTicketModalContactPage.fieldLabel")}
variant="outlined"
autoFocus
onChange={e => setSearchParam(e.target.value)}
onKeyPress={e => {
if (loading || !selectedContact) return;
else if (e.key === "Enter") {
handleSaveTicket(selectedContact.id);
}
}}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Grid>
)
}
return null;
}

return (
<>
<ContactModal
open={contactModalOpen}
initialValues={newContact}
onClose={handleCloseContactModal}
onSave={handleAddNewContactTicket}
></ContactModal>
<Dialog open={modalOpen} onClose={handleClose}>
<DialogTitle id="form-dialog-title">
{i18n.t("newTicketModal.title")}
</DialogTitle>
<DialogContent dividers>
<Grid style={{ width: 300 }} container spacing={2}>
{renderContactAutocomplete()}
<Grid xs={12} item>
<Select
fullWidth
displayEmpty
variant="outlined"
value={selectedQueue}
onChange={(e) => {
setSelectedQueue(e.target.value)
}}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={() => {
if (selectedQueue === "") {
return "Selecione uma fila"
}
const queue = user.queues.find(q => q.id === selectedQueue)
return queue.name
}}
>
{user.queues?.length > 0 &&
user.queues.map((queue, key) => (
<MenuItem dense key={key} value={queue.id}>
<ListItemText primary={queue.name} />
</MenuItem>
))}
</Select>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={loading}
variant="outlined"
>
{i18n.t("newTicketModal.buttons.cancel")}
</Button>
<ButtonWithSpinner
variant="contained"
type="button"
disabled={!selectedContact}
onClick={() => handleSaveTicket(selectedContact.id)}
color="primary"
loading={loading}
>
{i18n.t("newTicketModal.buttons.ok")}
</ButtonWithSpinner>
</DialogActions>
</Dialog>
</>
);
};

export default NewTicketModalPageContact;
Loading

0 comments on commit f08c195

Please sign in to comment.