src/pages/tickets/ticketPage.tsx
/* eslint-disable max-lines */
import React, { useCallback, useEffect, useState, useMemo } from "react";
import * as icons from "react-icons/ai";
import { connect, useSelector } from "react-redux";
import { useAppDispatch, useAppSelector } from "../../hooks/hooks";
import { Link, useNavigate } from "react-router-dom";
import { HiDotsVertical } from "react-icons/hi";
import * as AiIcons from "react-icons/ai";
import { ProgramSkeleton } from "../../skeletons/programSkeleton";
import Select from "react-select";
import {
DOTS,
useCustomPagination,
} from "../../components/Pagination/useCustomPagination";
import { toast, ToastContainer } from "react-toastify";
import { useTheme } from "../../hooks/darkmode";
import { debounce } from "lodash";
import {
createTicket,
getAllTickets,
getUserTickets,
} from "../../redux/actions/ticketActions";
import {
getAllFilteredTickets,
getAllTicketAttributes,
} from "../../redux/actions/filterTicketsAction";
import TicketPagination from "./ticketPagination";
import CreateTicketModal from "./createTicketModal";
import SearchFilter from "./ticketSearch";
import MobileTicketCard from "./mobileTicket";
import TicketTable from "./ticketTable";
import { useDispatch } from "react-redux";
const TicketFilters = ({ onFilterChange, fetchTickets, page, itemsPerPage, theme }) => {
const dispatch = useDispatch();
const [filterAttribute, setFilterAttribute] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const [All] = useState(false);
const ticketFilterOptions = [
{ value: "title", label: "Subject" },
{ value: "status", label: "Status" },
{ value: "", label: "Filter by" },
];
const debouncedSearch = useCallback(
debounce(async (term: string) => {
if (!filterAttribute) {
toast.error("Please select a filter attribute");
return;
}
if (!term) {
onFilterChange(false);
await fetchTickets();
return;
}
onFilterChange(true);
try {
await dispatch(
getAllFilteredTickets({
page: page + 1,
itemsPerPage,
All,
filterAttribute,
wordEntered: term,
})
);
} catch (error) {
toast.error("Failed to fetch filtered tickets");
}
}, 300),
[filterAttribute, page, itemsPerPage, All, fetchTickets, onFilterChange]
);
const handleSearchChange = (e) => {
const searchTerm = e.target.value;
setSearchTerm(searchTerm);
debouncedSearch(searchTerm);
};
const handleKeyDown = async (e) => {
if (e.key === "Enter") {
if (filterAttribute === "" || filterAttribute === null) {
toast.error("Please select a filter attribute");
return;
}
const searchTerm = e.target.value;
debouncedSearch(searchTerm);
}
};
return (
<SearchFilter
options={ticketFilterOptions}
filterAttribute={filterAttribute}
setFilterAttribute={setFilterAttribute}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleKeyDown={handleKeyDown}
theme={theme}
/>
);
};
const TicketPage = (props: any) => {
const dispatch = useAppDispatch();
const { theme, setTheme } = useTheme();
const [createTicketModal, setCreateTicketModal] = useState(false);
const [page, setPage] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [actionsList, setActionsList] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isFiltering, setIsFiltering] = useState(false);
const tickets = useSelector((state: any) => state.tickets?.tickets || []);
const filteredTickets = useSelector(
(state: any) => state.filteredTickets?.filteredTickets || []
);
const columns = [
{ key: 'title', header: 'Subject' },
{ key: 'status', header: 'Status' },
{
key: 'updatedAt',
header: 'Last Update',
render: (ticket: any) => new Date(parseInt(ticket.updatedAt)).toLocaleDateString(),
},
];
const applicantActionLinks = [
{ label: 'View', to: '/applicant/ticket/{id}' },
{ label: 'Reply', to: '/applicant/ticket/{id}/reply' },
];
const displayTickets = useMemo(() => {
if (isFiltering) {
return filteredTickets.sort((a: any, b: any) => b.updatedAt - a.updated);
}
return tickets.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
}, [isFiltering, filteredTickets, tickets]);
const paginationRange = useCustomPagination({
totalPageCount: Math.ceil(filteredTickets?.data?.length / itemsPerPage),
currentPage: page,
});
const fetchTickets = useCallback(async () => {
if (isFiltering) return;
setIsLoading(true);
try {
await dispatch(getUserTickets());
} catch (error) {
toast.error("Failed to fetch tickets");
} finally {
setIsLoading(false);
}
}, [dispatch, isFiltering]);
useEffect(() => {
fetchTickets();
}, [fetchTickets]);
const handleCreateTicket = async (ticketData) => {
await dispatch(createTicket(ticketData.title, ticketData.body));
};
const toggleActions = (id) => {
setActionsList((prev) => (prev === id ? null : id));
};
if (isLoading) {
return <ProgramSkeleton />;
}
return (
<>
<ToastContainer />
<CreateTicketModal
isOpen={createTicketModal}
onClose={() => setCreateTicketModal(false)}
onSubmit={handleCreateTicket}
/>
<div className="flex flex-col w-[100%]">
<div className="flex flex-row">
<div className="w-full">
<div className="bg-light-bg dark:bg-dark-frame-bg h-screen">
<div className="w-full px-4 sm:px-6 lg:px-8 py-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-4 sm:space-y-0 sm:space-x-4">
<div className="w-full sm:w-auto">
<button
onClick={() => setCreateTicketModal(true)}
className="flex items-center justify-center w-full sm:w-auto bg-primary dark:bg-[#56C870] rounded-md py-2 px-4 text-white font-medium cursor-pointer hover:opacity-90 transition-opacity"
>
<icons.AiOutlinePlus className="mr-2" /> New Ticket
</button>
</div>
<TicketFilters
onFilterChange={setIsFiltering}
fetchTickets={fetchTickets}
page={page}
itemsPerPage={itemsPerPage}
theme={theme}
/>
</div>
</div>
<div className="px-8">
<div className="bg-white dark:bg-dark-bg shadow-lg px-5 py-8 rounded-md w-[100%] mx-auto">
<div>
<div className="-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto">
<div className="hidden md_:inline-block w-full h-auto lg:min-w-full shadow rounded-lg overflow-y-hidden">
<TicketTable
columns={columns}
data={displayTickets}
actionLinks={applicantActionLinks}
onActionToggle={toggleActions}
actionsList={actionsList}
/>
</div>
<div className="flex md_:hidden flex-col gap-4 w-full rounded-lg">
<label className="text-left text-black-text dark:text-white text-lg font-bold">
Tickets
</label>
{displayTickets &&
displayTickets.map((ticket: any) => (
<MobileTicketCard ticket={ticket} />
))}
</div>
</div>
</div>
{tickets && (
<TicketPagination
itemsPerPage={itemsPerPage}
setItemsPerPage={setItemsPerPage}
page={page}
setPage={setPage}
paginationRange={paginationRange}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
const mapState = (state: any) => ({
tickets: state.tickets.tickets,
currentTicket: state.tickets.currentTicket,
loading: state.tickets.loading,
error: state.tickets.error,
});
export default connect(mapState, {
getAllFilteredTickets,
getUserTickets,
createTicket,
})(TicketPage);