feat: tickets and messages search concluded

This commit is contained in:
canove
2020-08-06 15:29:25 -03:00
parent 527e7c5375
commit bfd5c4f1a3
6 changed files with 127 additions and 80 deletions

View File

@@ -8,7 +8,15 @@ const Message = require("../models/Message");
const { getIO } = require("../libs/socket"); const { getIO } = require("../libs/socket");
exports.index = async (req, res) => { exports.index = async (req, res) => {
const { status = "", date = "", searchParam = "" } = req.query; const {
pageNumber = 1,
status = "",
date = "",
searchParam = "",
} = req.query;
let limit = 20;
let offset = limit * (pageNumber - 1);
let whereCondition = {}; let whereCondition = {};
let includeCondition = [ let includeCondition = [
@@ -41,6 +49,7 @@ exports.index = async (req, res) => {
), ),
}, },
required: false, required: false,
duplicating: false,
}, },
]; ];
@@ -78,13 +87,16 @@ exports.index = async (req, res) => {
}; };
} }
const tickets = await Ticket.findAll({ const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition, where: whereCondition,
distinct: true,
include: includeCondition, include: includeCondition,
limit,
offset,
order: [["updatedAt", "DESC"]], order: [["updatedAt", "DESC"]],
}); });
return res.json(tickets); return res.json({ count, tickets });
}; };
exports.store = async (req, res) => { exports.store = async (req, res) => {

View File

@@ -19,6 +19,7 @@
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-infinite-scroll-reverse": "^1.0.3", "react-infinite-scroll-reverse": "^1.0.3",
"react-infinite-scroller": "^1.2.4",
"react-linkify": "^1.0.0-alpha", "react-linkify": "^1.0.0-alpha",
"react-modal-image": "^2.5.0", "react-modal-image": "^2.5.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",

View File

@@ -3,6 +3,7 @@ import { useHistory, useParams } from "react-router-dom";
import openSocket from "socket.io-client"; import openSocket from "socket.io-client";
import { parseISO, format } from "date-fns"; import { parseISO, format } from "date-fns";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import InfiniteScroll from "react-infinite-scroller";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors";
@@ -60,7 +61,7 @@ const useStyles = makeStyles(theme => ({
width: 120, // a number of your choice width: 120, // a number of your choice
}, },
openTicketsList: { halfTicketsList: {
height: "50%", height: "50%",
overflowY: "scroll", overflowY: "scroll",
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
@@ -74,7 +75,7 @@ const useStyles = makeStyles(theme => ({
borderTop: "1px solid rgba(0, 0, 0, 0.12)", borderTop: "1px solid rgba(0, 0, 0, 0.12)",
}, },
closedTicketsList: { fullHeightTicketsList: {
flex: 1, flex: 1,
overflowY: "scroll", overflowY: "scroll",
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
@@ -245,6 +246,9 @@ const TicketsList = () => {
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
const [showAllTickets, setShowAllTickets] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [count, setCount] = useState(0);
useEffect(() => { useEffect(() => {
if (!("Notification" in window)) { if (!("Notification" in window)) {
console.log("This browser doesn't support notifications"); console.log("This browser doesn't support notifications");
@@ -255,17 +259,21 @@ const TicketsList = () => {
useEffect(() => { useEffect(() => {
setTickets([]); setTickets([]);
}, [searchParam]); setPageNumber(1);
}, [searchParam, tab]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => { const fetchContacts = async () => {
try { try {
const res = await api.get("/tickets", { const { data } = await api.get("/tickets", {
params: { searchParam, status: tab }, params: { searchParam, pageNumber, status: tab },
}); });
setTickets(res.data); setTickets(prevState => {
return [...prevState, ...data.tickets];
});
setCount(data.count);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -274,7 +282,9 @@ const TicketsList = () => {
fetchContacts(); fetchContacts();
}, 1000); }, 1000);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchParam, token, tab]); }, [searchParam, pageNumber, token, tab]);
console.log(pageNumber);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
@@ -316,6 +326,11 @@ const TicketsList = () => {
}; };
}, [ticketId, userId, history]); }, [ticketId, userId, history]);
const loadMore = () => {
if (loading) return;
setPageNumber(prevPageNumber => prevPageNumber + 1);
};
const updateTickets = ({ ticket }) => { const updateTickets = ({ ticket }) => {
setTickets(prevState => { setTickets(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === ticket.id); const ticketIndex = prevState.findIndex(t => t.id === ticket.id);
@@ -388,7 +403,6 @@ const TicketsList = () => {
const handleSearchContact = e => { const handleSearchContact = e => {
if (e.target.value === "") { if (e.target.value === "") {
// setTab("open");
setSearchParam(e.target.value.toLowerCase()); setSearchParam(e.target.value.toLowerCase());
return; return;
} }
@@ -423,8 +437,7 @@ const TicketsList = () => {
return ticketsFound; return ticketsFound;
}; };
console.log(tickets); const RenderTickets = ({ status, userId }) => {
const renderTickets = (status, userId) => {
const viewTickets = tickets.map(ticket => { const viewTickets = tickets.map(ticket => {
if ( if (
(ticket.status === status && ticket.userId === userId) || (ticket.status === status && ticket.userId === userId) ||
@@ -521,7 +534,7 @@ const TicketsList = () => {
else return null; else return null;
}); });
if (loading) { if (loading && status !== "all" && status !== "closed") {
return <TicketsSkeleton />; return <TicketsSkeleton />;
} else if ( } else if (
countTickets(status, userId) === "" && countTickets(status, userId) === "" &&
@@ -546,6 +559,22 @@ const TicketsList = () => {
} }
}; };
const RenderInfiniteScroll = ({ children, loadingKey }) => {
return (
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={!(tickets.length === count)}
useWindow={false}
initialLoad={false}
threshold={100}
loader={<TicketsSkeleton key={loadingKey} />}
>
<List style={{ paddingTop: 0 }}>{children}</List>
</InfiniteScroll>
);
};
return ( return (
<Paper elevation={0} variant="outlined" className={classes.contactsWrapper}> <Paper elevation={0} variant="outlined" className={classes.contactsWrapper}>
<NewTicketModal <NewTicketModal
@@ -586,55 +615,55 @@ const TicketsList = () => {
<SearchIcon className={classes.searchIcon} /> <SearchIcon className={classes.searchIcon} />
<InputBase <InputBase
className={classes.contactsSearchInput} className={classes.contactsSearchInput}
placeholder="Buscar contatos" placeholder="Pesquisar tickets e mensagens"
type="search" type="search"
onChange={handleSearchContact} onChange={handleSearchContact}
/> />
</div> </div>
</Paper> </Paper>
<TabPanel value={tab} index={"open"} className={classes.contactsWrapper}> <TabPanel value={tab} index={"open"} className={classes.contactsWrapper}>
<Paper square elevation={0} className={classes.openTicketsList}> <Paper square elevation={0} className={classes.halfTicketsList}>
<List style={{ paddingTop: 0 }}> <div className={classes.ticketsListHeader}>
<div className={classes.ticketsListHeader}> Atendendo
Atendendo <span className={classes.ticketsCount}>
<span className={classes.ticketsCount}> {countTickets("open", userId)}
{countTickets("open", userId)} </span>
</span> <div className={classes.ticketsListActions}>
<div className={classes.ticketsListActions}> <FormControlLabel
<FormControlLabel label="Todos"
label="Todos" labelPlacement="start"
labelPlacement="start" control={
control={ <Switch
<Switch size="small"
size="small" checked={showAllTickets}
checked={showAllTickets} onChange={e => setShowAllTickets(prevState => !prevState)}
onChange={e => setShowAllTickets(prevState => !prevState)} name="showAllTickets"
name="showAllTickets" color="primary"
color="primary" />
/> }
} />
/> <IconButton
<IconButton aria-label="add ticket"
aria-label="add ticket" onClick={e => setNewTicketModalOpen(true)}
onClick={e => setNewTicketModalOpen(true)} style={{ marginLeft: 20 }}
style={{ marginLeft: 20 }} >
> <AddIcon />
<AddIcon /> </IconButton>
</IconButton>
</div>
</div> </div>
{renderTickets("open", userId)} </div>
<List style={{ paddingTop: 0 }}>
<RenderTickets status="open" userId={userId} />
</List> </List>
</Paper> </Paper>
<Paper square elevation={0} className={classes.openTicketsList}> <Paper square elevation={0} className={classes.halfTicketsList}>
<div className={classes.ticketsListHeader}>
Aguardando
<span className={classes.ticketsCount}>
{countTickets("pending", null)}
</span>
</div>
<List style={{ paddingTop: 0 }}> <List style={{ paddingTop: 0 }}>
<div className={classes.ticketsListHeader}> <RenderTickets status="pending" userId={null} />
Aguardando
<span className={classes.ticketsCount}>
{countTickets("pending", null)}
</span>
</div>
{renderTickets("pending", null)}
</List> </List>
</Paper> </Paper>
</TabPanel> </TabPanel>
@@ -643,8 +672,10 @@ const TicketsList = () => {
index={"closed"} index={"closed"}
className={classes.contactsWrapper} className={classes.contactsWrapper}
> >
<Paper square elevation={0} className={classes.closedTicketsList}> <Paper square elevation={0} className={classes.fullHeightTicketsList}>
<List>{renderTickets("closed", userId)}</List> <RenderInfiniteScroll loadingKey="loading-closed">
<RenderTickets status="closed" userId={null} />
</RenderInfiniteScroll>
</Paper> </Paper>
</TabPanel> </TabPanel>
<TabPanel <TabPanel
@@ -652,25 +683,21 @@ const TicketsList = () => {
index={"search"} index={"search"}
className={classes.contactsWrapper} className={classes.contactsWrapper}
> >
<Paper square elevation={0} className={classes.closedTicketsList}> <Paper square elevation={0} className={classes.fullHeightTicketsList}>
{loading ? ( <>
<TicketsSkeleton /> {tickets.length === 0 && !loading ? (
) : ( <div className={classes.noTicketsDiv}>
<> <span className={classes.noTicketsTitle}>Nada encontrado</span>
{tickets.length === 0 ? ( <p className={classes.noTicketsText}>
<div className={classes.noTicketsDiv}> Tente buscar por outro termo.
<span className={classes.noTicketsTitle}> </p>
"Nada encontrado" </div>
</span> ) : (
<p className={classes.noTicketsText}> <RenderInfiniteScroll loadingKey="loading-all">
Tente buscar por outro termo. <RenderTickets status="all" />
</p> </RenderInfiniteScroll>
</div> )}
) : ( </>
<List>{renderTickets("all")}</List>
)}
</>
)}
</Paper> </Paper>
</TabPanel> </TabPanel>
<audio id="sound" preload="auto"> <audio id="sound" preload="auto">

View File

@@ -18,7 +18,7 @@ const TicketsSkeleton = () => {
secondary={<Skeleton animation="wave" height={20} width={90} />} secondary={<Skeleton animation="wave" height={20} width={90} />}
/> />
</ListItem> </ListItem>
<Divider /> <Divider variant="inset" />
<ListItem dense> <ListItem dense>
<ListItemAvatar> <ListItemAvatar>
<Skeleton animation="wave" variant="circle" width={40} height={40} /> <Skeleton animation="wave" variant="circle" width={40} height={40} />
@@ -28,7 +28,7 @@ const TicketsSkeleton = () => {
secondary={<Skeleton animation="wave" height={20} width={120} />} secondary={<Skeleton animation="wave" height={20} width={120} />}
/> />
</ListItem> </ListItem>
<Divider /> <Divider variant="inset" />
<ListItem dense> <ListItem dense>
<ListItemAvatar> <ListItemAvatar>
<Skeleton animation="wave" variant="circle" width={40} height={40} /> <Skeleton animation="wave" variant="circle" width={40} height={40} />
@@ -38,7 +38,7 @@ const TicketsSkeleton = () => {
secondary={<Skeleton animation="wave" height={20} width={90} />} secondary={<Skeleton animation="wave" height={20} width={90} />}
/> />
</ListItem> </ListItem>
<Divider /> <Divider variant="inset" />
</> </>
); );
}; };

View File

@@ -36,10 +36,10 @@ const Chart = () => {
useEffect(() => { useEffect(() => {
const feathTickets = async () => { const feathTickets = async () => {
try { try {
const res = await api.get("/tickets", { const { data } = await api.get("/tickets", {
params: { date: new Date().toISOString() }, params: { date: new Date().toISOString() },
}); });
let ticketsData = res.data; const ticketsData = data.tickets;
setTickets(ticketsData); setTickets(ticketsData);
setChartData(prevState => { setChartData(prevState => {
let aux = [...prevState]; let aux = [...prevState];

View File

@@ -9012,7 +9012,7 @@ prompts@^2.0.1:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.4" sisteransi "^1.0.4"
prop-types@15.7.2, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -9249,6 +9249,13 @@ react-infinite-scroll-reverse@^1.0.3:
dependencies: dependencies:
prop-types "15.7.2" prop-types "15.7.2"
react-infinite-scroller@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9"
integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw==
dependencies:
prop-types "^15.5.8"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4: react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"