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
+
+ (window.location.href = "/support/closed")}
+ >
+
+ View Closed
+
+ {user.isSuperAdmin && (
+ setShowSettingsModal(true)}
+ basic
+ className="ml-2"
+ >
+
+ Support Center Settings
+
+ )}
+
+
+
+
+
+
+
+
+
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 (
+
+
+
+ View
+
+ {record.status === "open" && (
+ openAssignModal(record.uuid)}
+ className="inline-flex !ml-2"
+ >
+
+ Assign
+
+ )}
+
+ );
+ },
+ },
+ ]}
+ />
+
+
+
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 = () => {
/>
-