From 55bb2b57f96c3b27c5e448bceb7f3cc6f409b554 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:58:31 -0700 Subject: [PATCH 1/6] feat: added account creation banner to support ticket form --- .../conductor/support/SupportCreateTicket.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/client/src/screens/conductor/support/SupportCreateTicket.tsx b/client/src/screens/conductor/support/SupportCreateTicket.tsx index ede1247f..81d504fa 100644 --- a/client/src/screens/conductor/support/SupportCreateTicket.tsx +++ b/client/src/screens/conductor/support/SupportCreateTicket.tsx @@ -6,6 +6,7 @@ import CreateTicketFlow from "../../../components/support/CreateTicketFlow"; import useSystemAnnouncement from "../../../hooks/useSystemAnnouncement"; import SystemAnnouncement from "../../../components/util/SystemAnnouncement"; import { useDocumentTitle } from "usehooks-ts"; +import { Message } from 'semantic-ui-react'; const SupportCreateTicket = () => { useDocumentTitle("LibreTexts | Contact Support"); @@ -46,6 +47,27 @@ const SupportCreateTicket = () => {

Submit a request to get help from our team.

+
+ + +
+ Important: +

+ Please do not submit a support ticket for account creation. You can visit {' '} + + https://register.libretexts.org + + {' '} to create an account and complete instructor verification (if applicable). +

+
+
+
+
<> {!isLoggedIn && !guestMode && (
From f0b2bf381afc4136ca2c0f886ddc6f1ae0819059 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:50:29 -0700 Subject: [PATCH 2/6] feat: removed recently edited projects --- .../Home/PinnedProjects/PinnedProjects.tsx | 21 ++++++--- client/src/screens/conductor/Home/index.tsx | 44 ++----------------- 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx index f667eae0..6372c646 100644 --- a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx +++ b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx @@ -40,7 +40,7 @@ const PinnedProjects: React.FC = () => { const panes = useMemo(() => { if (!data) return []; - const classList = "xl:!ml-24 2xl:!ml-4 3xl:!ml-0 !max-h-[500px] overflow-y-auto xl:!mr-1" + const classList = "xl:!ml-24 2xl:!ml-4 3xl:!ml-0 !max-h-[500px] xl:!mr-1 !pt-1" const allItemsLength = data ?.map((i) => i.projects?.length) .reduce((acc, curr) => acc + (curr || 0), 0); @@ -99,12 +99,11 @@ const PinnedProjects: React.FC = () => { ); }, }); - return items; }, [data]); return ( - 0} loading={isLoading} className="!pb-10"> + 0} loading={isLoading} className="!pb-10 mt-4">

@@ -128,10 +127,18 @@ const PinnedProjects: React.FC = () => { />

- +
+
+ +
+
); }; diff --git a/client/src/screens/conductor/Home/index.tsx b/client/src/screens/conductor/Home/index.tsx index 7fd58aec..2e03cc6f 100644 --- a/client/src/screens/conductor/Home/index.tsx +++ b/client/src/screens/conductor/Home/index.tsx @@ -199,11 +199,11 @@ const Home = () => { )} -
-
+
+
-
+
-
+

Announcements

From 9851b33ef05ca33bf3847f4381c473ccd7348f97 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:14:25 -0700 Subject: [PATCH 3/6] chore(ui): hide Student ID column from Central Identity Users list --- .../controlpanel/CentralIdentity/CentralIdentityUsers.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers.tsx b/client/src/screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers.tsx index 0be94e01..d27838a1 100644 --- a/client/src/screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers.tsx +++ b/client/src/screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers.tsx @@ -48,7 +48,6 @@ const CentralIdentityUsers = () => { { key: "email", text: "Email" }, { key: "userType", text: "User Type" }, { key: "verification", text: "Verification Status" }, - { key: "studentId", text: "Student ID" }, { key: "Auth Source", text: "Auth Source" }, { key: "Actions", text: "Actions" }, ]; @@ -236,7 +235,7 @@ const CentralIdentityUsers = () => { { setSearchInput(e.target.value); getUsersDebounced(e.target.value); From aa40e8740cc3a75f154b77b1c2a8bc43330b9e92 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:27:56 -0700 Subject: [PATCH 4/6] fix(support): added horizontal scrollbar in support dashboard --- .../src/components/support/StaffDashboard.tsx | 550 ++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 client/src/components/support/StaffDashboard.tsx diff --git a/client/src/components/support/StaffDashboard.tsx b/client/src/components/support/StaffDashboard.tsx new file mode 100644 index 00000000..3e4db606 --- /dev/null +++ b/client/src/components/support/StaffDashboard.tsx @@ -0,0 +1,550 @@ +import { useState, useEffect, lazy } from "react"; +import { Button, Dropdown, Icon, Input } from "semantic-ui-react"; +import useGlobalError from "../error/ErrorHooks"; +import { GenericKeyTextValueObj, SupportTicket } from "../../types"; +import axios from "axios"; +import { format, parseISO } from "date-fns"; +import TicketStatusLabel from "./TicketStatusLabel"; +import { getRequesterText } from "../../utils/kbHelpers"; +import { PaginationWithItemsSelect } from "../util/PaginationWithItemsSelect"; +import { useTypedSelector } from "../../state/hooks"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { capitalizeFirstLetter } from "../util/HelperFunctions"; +import { getPrettySupportTicketCategory } from "../../utils/supportHelpers"; +import { useNotifications } from "../../context/NotificationContext"; +import CopyButton from "../util/CopyButton"; +import { Link } from "react-router-dom"; +import SupportCenterTable from "./SupportCenterTable"; +import useDebounce from "../../hooks/useDebounce"; +import LoadingSpinner from "../LoadingSpinner"; +const AssignTicketModal = lazy(() => import("./AssignTicketModal")); +const SupportCenterSettingsModal = lazy( + () => import("./SupportCenterSettingsModal"), +); + +type SupportMetrics = { + totalOpenTickets: number; + lastSevenTicketCount: number; + avgDaysToClose: string; +}; + +const StaffDashboard = () => { + const { handleGlobalError } = useGlobalError(); + const { addNotification } = useNotifications(); + const queryClient = useQueryClient(); + const user = useTypedSelector((state) => state.user); + const { debounce } = useDebounce(); + + const [queryInputString, setQueryInputString] = useState(""); + const [query, setQuery] = useState(""); + const [activePage, setActivePage] = useState(1); + const [activeSort, setActiveSort] = useState("opened"); + const [totalPages, setTotalPages] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(25); + const [totalItems, setTotalItems] = useState(0); + const [showSettingsModal, setShowSettingsModal] = useState(false); + const [showAssignModal, setShowAssignModal] = useState(false); + const [selectedTicketId, setSelectedTicketId] = useState(""); + const [assigneeFilter, setAssigneeFilter] = useState(""); + const [priorityFilter, setPriorityFilter] = useState(""); + const [categoryFilter, setCategoryFilter] = useState(""); + const [filterOptions, setFilterOptions] = useState<{ + assignee: GenericKeyTextValueObj[]; + priority: GenericKeyTextValueObj[]; + category: GenericKeyTextValueObj[]; + }>({ + assignee: [], + priority: [], + category: [], + }); + + const { data: openTickets, isFetching } = useQuery({ + queryKey: [ + "openTickets", + activePage, + itemsPerPage, + activeSort, + assigneeFilter, + priorityFilter, + categoryFilter, + query, + ], + queryFn: () => + getOpenTickets({ + query: queryInputString, + page: activePage, + items: itemsPerPage, + sort: activeSort, + assigneeFilter, + priorityFilter, + categoryFilter, + }), + keepPreviousData: true, + staleTime: 1000 * 60 * 2, // 2 minutes + }); + + const { data: supportMetrics, isFetching: isFetchingMetrics } = + useQuery({ + queryKey: ["supportMetrics"], + queryFn: getSupportMetrics, + staleTime: 1000 * 60 * 2, // 2 minutes + }); + + useEffect(() => { + setActivePage(1); // Reset to first page when itemsPerPage changes + }, [itemsPerPage]); + + async function getOpenTickets({ + query, + page, + items, + sort, + assigneeFilter, + priorityFilter, + categoryFilter, + }: { + query: string; + page: number; + items: number; + sort: string; + assigneeFilter?: string; + priorityFilter?: string; + categoryFilter?: string; + }) { + try { + const res = await axios.get("/support/ticket/open", { + params: { + ...(query?.length > 3 && { query: query }), + page: page, + limit: items, + sort: sort, + ...(assigneeFilter && { assignee: assigneeFilter }), + ...(priorityFilter && { priority: priorityFilter }), + ...(categoryFilter && { category: categoryFilter }), + }, + }); + if (res.data.err) { + throw new Error(res.data.errMsg); + } + + if (!res.data.tickets || !Array.isArray(res.data.tickets)) { + throw new Error("Invalid response from server"); + } + + setTotalItems(res.data.total); + setTotalPages(Math.ceil(res.data.total / items)); + + const CLEAR_OPTION = { key: "clear", text: "Clear", value: "" }; + if (res.data.filters) { + const _assignee = Array.isArray(res.data.filters.assignee) + ? [CLEAR_OPTION, ...res.data.filters.assignee] + : []; + const _priority = Array.isArray(res.data.filters.priority) + ? [CLEAR_OPTION, ...res.data.filters.priority] + : []; + const _category = Array.isArray(res.data.filters.category) + ? [CLEAR_OPTION, ...res.data.filters.category] + : []; + + setFilterOptions({ + assignee: _assignee, + priority: _priority, + category: _category, + }); + } else { + setFilterOptions({ + assignee: [], + priority: [], + category: [], + }); + } + + return (res.data.tickets as SupportTicket[]) ?? []; + } catch (err) { + handleGlobalError(err); + return []; + } + } + + async function getSupportMetrics(): Promise { + try { + const res = await axios.get("/support/metrics"); + if (res.data.err) { + throw new Error(res.data.errMsg); + } + + if (!res.data.metrics) { + throw new Error("Invalid response from server"); + } + + // Convert avgMins to days + const avgMins = res.data.metrics.avgMinsToClose ?? 0; + const avgDays = avgMins / (60 * 24); + return { + totalOpenTickets: res.data.metrics.totalOpenTickets ?? 0, + lastSevenTicketCount: res.data.metrics.lastSevenTicketCount ?? 0, + avgDaysToClose: Math.floor(avgDays).toString() ?? "0", + }; + } catch (err) { + handleGlobalError(err); + return { + totalOpenTickets: 0, + lastSevenTicketCount: 0, + avgDaysToClose: "0", + }; + } + } + + const debouncedQueryUpdate = debounce( + (searchString: string) => setQuery(searchString), + 300, + ); + + function openAssignModal(ticketId: string) { + setSelectedTicketId(ticketId); + setShowAssignModal(true); + } + + function onCloseAssignModal() { + setShowAssignModal(false); + setSelectedTicketId(""); + queryClient.invalidateQueries(["openTickets"]); + } + + function handleFilterChange(filter: string, value: string) { + setActivePage(1); // Reset to first page on filter change + switch (filter) { + case "assignee": + setAssigneeFilter(value); + break; + case "priority": + setPriorityFilter(value); + break; + case "category": + setCategoryFilter(value); + break; + default: + break; + } + } + + function getAssigneeName(uuid: string) { + const user = openTickets?.find((t) => + t.assignedUsers?.find((u) => u.uuid === uuid), + ); + if (user) { + return user.assignedUsers?.find((u) => u.uuid === uuid)?.firstName; + } else { + return ""; + } + } + + const DashboardMetric = ({ + metric, + title, + loading = false, + }: { + metric: string; + title: string; + loading?: boolean; + }) => ( +
+

{loading ? : metric}

+

{title}

+
+ ); + + return ( +
+
+

Staff Dashboard

+
+ + {user.isSuperAdmin && ( + + )} +
+
+
+ + + +
+
+

Open/In Progress Tickets

+
+
+ + + {filterOptions.assignee.map((a) => ( + handleFilterChange("assignee", a.value)} + > + {a.text} + + ))} + + + + + {filterOptions.priority.map((p) => ( + handleFilterChange("priority", p.value)} + > + {p.text} + + ))} + + + + + {filterOptions.category.map((c) => ( + handleFilterChange("category", c.value)} + > + {c.text} + + ))} + + +
+ { + setQueryInputString(e.target.value); + debouncedQueryUpdate(e.target.value); + }} + /> +
+ + + loading={isFetching} + data={openTickets} + columns={[ + { + accessor: "uuid", + title: "ID", + render(record, index) { + return ( + <> + {record.uuid.slice(-7)} + + {({ copied, copy }) => ( + { + copy(); + addNotification({ + message: "Ticket ID copied to clipboard", + type: "success", + duration: 2000, + }); + }} + color={copied ? "green" : "blue"} + /> + )} + + + ); + }, + }, + { + accessor: "timeOpened", + title: "Date Opened", + render(record) { + return format( + parseISO(record.timeOpened), + "MM/dd/yyyy hh:mm aa", + ); + }, + }, + { + accessor: "title", + title: "Subject", + className: "!w-full !max-w-[40rem] break-words truncate", + render(record) { + return record.title; + }, + }, + { + accessor: "category", + title: "Category", + render(record) { + return getPrettySupportTicketCategory(record.category); + }, + }, + { + accessor: "user", + title: "Requester", + render(record) { + return getRequesterText(record); + }, + }, + { + accessor: "assignedUsers", + title: "Assigned To", + render(record) { + return record.assignedUsers + ? record.assignedUsers.map((u) => u.firstName).join(", ") + : "Unassigned"; + }, + }, + { + accessor: "priority", + render(record) { + return capitalizeFirstLetter(record.priority); + }, + }, + { + accessor: "status", + render(record) { + return ; + }, + }, + { + accessor: "actions", + render(record) { + return ( +
+ + {record.status === "open" && ( + + )} +
+ ); + }, + }, + ]} + /> + +
+ setShowSettingsModal(false)} + /> + {selectedTicketId && ( + + )} +
+ ); +}; + +export default StaffDashboard; From 81a5bc71dbea5764dbaba55076f0f25ab5347c5b Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Thu, 3 Jul 2025 00:29:51 -0700 Subject: [PATCH 5/6] fix(home): add a 3-row layout for XL-screens --- .../Home/PinnedProjects/PinnedProjects.tsx | 14 ++++++++------ .../src/components/projects/ProjectCard/index.tsx | 2 +- client/src/screens/conductor/Home/index.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx index 6372c646..ce7ce072 100644 --- a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx +++ b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx @@ -20,6 +20,8 @@ interface PinnedProjectsInterface {} const PinnedProjects: React.FC = () => { const { openModal, closeAllModals } = useModals(); const isTailwindLg = useMediaQuery({ minWidth: 1024 }); + const isXL = useMediaQuery({ minWidth: 1280 }); + console.log("isXL", isXL); const { data, isLoading } = usePinnedProjects(); const onShowPinnedModal = () => { @@ -64,7 +66,7 @@ const PinnedProjects: React.FC = () => { return ; } return ( - + {i.projects?.map((item) => typeof item === "string" ? null : ( @@ -86,7 +88,7 @@ const PinnedProjects: React.FC = () => { return ; } return ( - + {data?.map((i) => { if (typeof i.projects === "string") return null; return i.projects?.map((item) => @@ -100,10 +102,10 @@ const PinnedProjects: React.FC = () => { }, }); return items; - }, [data]); + }, [data, isXL]); return ( - 0} loading={isLoading} className="!pb-10 mt-4"> + 0} loading={isLoading} className="!pb-10 mt-4 flex-1 flex flex-col min-h-0">

@@ -127,8 +129,8 @@ const PinnedProjects: React.FC = () => { />

-
-
+
+
-
+
{
)} -
-
+
+
-
+
-
+

Announcements

From 1bbe40a5a975282f693b9860beddf78c51d1df87 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Thu, 3 Jul 2025 00:31:43 -0700 Subject: [PATCH 6/6] refactor: removed debugging statements --- client/src/components/Home/PinnedProjects/PinnedProjects.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx index ce7ce072..36d75692 100644 --- a/client/src/components/Home/PinnedProjects/PinnedProjects.tsx +++ b/client/src/components/Home/PinnedProjects/PinnedProjects.tsx @@ -21,7 +21,6 @@ const PinnedProjects: React.FC = () => { const { openModal, closeAllModals } = useModals(); const isTailwindLg = useMediaQuery({ minWidth: 1024 }); const isXL = useMediaQuery({ minWidth: 1280 }); - console.log("isXL", isXL); const { data, isLoading } = usePinnedProjects(); const onShowPinnedModal = () => {