atlp-rwanda/atlp-devpulse-fn

View on GitHub
src/pages/tickets/ticketPage.tsx

Summary

Maintainability
A
0 mins
Test Coverage
/* 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);