atlp-rwanda/atlp-devpulse-fn

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

Summary

Maintainability
A
0 mins
Test Coverage
import React, { useCallback, useEffect, useState, useMemo } from "react";
import { toast, ToastContainer } from "react-toastify";
import { useTheme } from "../../hooks/darkmode";
import {useCustomPagination} from "../../components/Pagination/useCustomPagination";
import Select from "react-select";
import { Link, useNavigate } from "react-router-dom";
import { HiDotsVertical } from "react-icons/hi";
import { useDispatch, useSelector } from "react-redux";
import { getAllTickets } from "../../redux/actions/ticketActions";
import { ProgramSkeleton } from "../../skeletons/programSkeleton";
import {
  getAllFilteredTickets,
  getAllTicketAttributes,
} from "../../redux/actions/filterTicketsAction";
import {debounce} from "lodash"
import TicketPagination from "./ticketPagination";
import SearchFilter from "./ticketSearch";
import MobileTicketCard from "./mobileTicket";
import TicketTable from "./ticketTable";

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: "author", label: "Author" },
    { 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}
      placeholder="Search"
      containerClassName="my-4"
    />
  );
};

const AdminTicketPage = (props: any) => {
  const { theme } = useTheme();
  const dispatch = useDispatch();
  const tickets = useSelector(
    (state: any) => state.tickets?.tickets || []
  );
  const [actionsList, setActionsList] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [page, setPage] = useState(0);
  const [itemsPerPage, setItemsPerPage] = useState(10);
  const { allfilteredTickets, count } = props;
  const [isFiltering, setIsFiltering] = useState(false);
  const filteredTickets = useSelector((state: any) => state.filteredTickets?.filteredTickets || []);

  const columns = [
    { key: 'title', header: 'Subject' },
    { 
      key: 'author.firstname', 
      header: 'Author Name',
      render: (ticket: any) => {
        const fullName = `${ticket.author?.firstname || ''} ${ticket.author?.lastname || ''}`.trim();
        return fullName || '-';
      }
    },
    { key: 'author.email', header: 'Author Email', render: (item: any) => item.author?.email || '-'},    
    { key: 'status', header: 'Status' },
    { key: 'updatedAt', header: 'Last Update' },
  ];

  const adminActionLinks = [
    { label: 'View', to: '/admin/ticket/{id}' },
    { label: 'Resolve', to: '/admin/ticket/{id}/resolve' },
  ];


  const displayTickets = useMemo(() => {
    if (isFiltering) {
      return filteredTickets.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
    }
    return tickets.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
  }, [isFiltering, filteredTickets, tickets]);

  const fetchTickets = useCallback(async () => {
    setIsLoading(true);
    try {
      await dispatch(getAllTickets());
    } catch (error) {
      toast.error("Failed to fetch tickets");
    } finally {
      setIsLoading(false);
    }
  }, [dispatch]);

  useEffect(() => {
    fetchTickets();
  }, [fetchTickets]);

  const toggleActions = (id) => {
    setActionsList((prev) => (prev === id ? null : id));
  };

  const paginationRange = useCustomPagination({
    totalPageCount: Math.ceil(allfilteredTickets?.data?.length / itemsPerPage),
    currentPage: page,
  });

  if (isLoading) {
    return <ProgramSkeleton />;
  }

  return (
    <>
      <ToastContainer />

      <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">
                <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={adminActionLinks}
                          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>
                        {tickets &&
                          tickets.map((ticket: any) => (
                            <MobileTicketCard
                              ticket={ticket}
                              isAdmin={true}
                              showAuthor={true}
                            />
                          ))}
                      </div>
                    </div>
                  </div>
                  {filteredTickets && (
                    <TicketPagination
                      itemsPerPage={itemsPerPage}
                      setItemsPerPage={setItemsPerPage}
                      page={page}
                      setPage={setPage}
                      paginationRange={paginationRange}
                    />
                  )}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  );
};

export default AdminTicketPage;