diff --git a/csm_web/frontend/src/components/App.tsx b/csm_web/frontend/src/components/App.tsx index 00e27a42..0527ef90 100644 --- a/csm_web/frontend/src/components/App.tsx +++ b/csm_web/frontend/src/components/App.tsx @@ -9,6 +9,7 @@ import { emptyRoles, Roles } from "../utils/user"; import CourseMenu from "./CourseMenu"; import Home from "./Home"; import Policies from "./Policies"; +import CoordTable from "./coord_interface/CoordTable"; import { DataExport } from "./data_export/DataExport"; import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher"; import { Resources } from "./resource_aggregation/Resources"; @@ -38,6 +39,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/csm_web/frontend/src/components/Home.tsx b/csm_web/frontend/src/components/Home.tsx index da7873e0..6a060adf 100644 --- a/csm_web/frontend/src/components/Home.tsx +++ b/csm_web/frontend/src/components/Home.tsx @@ -131,9 +131,11 @@ const CourseCard = ({ profiles }: CourseCardProps): React.ReactElement => { if (role === Role.COORDINATOR) { return ( - - - + <> + + + + ); } diff --git a/csm_web/frontend/src/components/SearchBar.tsx b/csm_web/frontend/src/components/SearchBar.tsx index 255a9766..64bc19d5 100644 --- a/csm_web/frontend/src/components/SearchBar.tsx +++ b/csm_web/frontend/src/components/SearchBar.tsx @@ -12,7 +12,7 @@ export const SearchBar = ({ className, refObject, onChange }: SearchBarProps) => return (
- +
); }; diff --git a/csm_web/frontend/src/components/coord_interface/ActionButton.tsx b/csm_web/frontend/src/components/coord_interface/ActionButton.tsx new file mode 100644 index 00000000..799fad48 --- /dev/null +++ b/csm_web/frontend/src/components/coord_interface/ActionButton.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useLocation, useNavigate } from "react-router-dom"; + +interface ActionButtonProps { + copyEmail: () => void; + reset: () => void; +} + +export default function ActionButton({ copyEmail, reset }: ActionButtonProps) { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const isStudents = pathname.includes("students"); + function changeURL() { + const newPath = isStudents ? pathname.replace("students", "mentors") : pathname.replace("mentors", "students"); + reset(); + navigate(newPath); + } + return ( +
+ + {isStudents ? ( + + ) : ( + + )} +
+ ); +} diff --git a/csm_web/frontend/src/components/coord_interface/CheckBox.tsx b/csm_web/frontend/src/components/coord_interface/CheckBox.tsx new file mode 100644 index 00000000..26e1aaba --- /dev/null +++ b/csm_web/frontend/src/components/coord_interface/CheckBox.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import styles from "../../css/coord_interface.scss"; + +interface CheckBoxProps { + id: string; + onClick?: (e: React.MouseEvent) => void; +} + +export function CheckBox({ id, onClick: onClick }: CheckBoxProps) { + return ( + +
+ + + + + + + +
+ + ); +} diff --git a/csm_web/frontend/src/components/coord_interface/CoordTable.tsx b/csm_web/frontend/src/components/coord_interface/CoordTable.tsx new file mode 100644 index 00000000..23004c75 --- /dev/null +++ b/csm_web/frontend/src/components/coord_interface/CoordTable.tsx @@ -0,0 +1,313 @@ +import React, { useEffect, useState } from "react"; +import { useLocation, useParams, useNavigate } from "react-router-dom"; +import { Mentor, Student, getCoordData } from "../../utils/queries/coord"; +import ActionButton from "./ActionButton"; +import { CheckBox } from "./CheckBox"; +import DropBox from "./DropBox"; +import { SearchBar } from "./SearchBar"; +import styles from "../../css/coord_interface.scss"; + +export default function CoordTable() { + const [tableData, setTableData] = useState([]); + const [searchData, setSearch] = useState([]); + const [selectedData, setSelected] = useState([]); + const [allSelected, setAllSelected] = useState(false); + const params = useParams(); + const courseId = Number(params.id); + const { pathname } = useLocation(); + const isStudents = pathname.includes("students"); + const [currentFilter, setCurrentFilter] = useState(null); + const sectionSizes = !isStudents + ? [...new Set(tableData.map(item => (item as Mentor).numStudents?.toString()))].sort() + : []; // Unique section sizes for mentors + const familyNames = !isStudents ? [...new Set(tableData.map(item => (item as Mentor).family))].sort() : []; // Unique family names for mentors + + // On load + useEffect(() => { + const fetchData = async () => { + const data = await getCoordData(courseId, isStudents); + setTableData(data); + setSearch(data); + }; + fetchData(); + }, [pathname]); + const navigate = useNavigate(); + + function reset() { + // Used for chainging url + setTableData([]); + setSearch([]); + setSelected([]); + setAllSelected(false); + const checkbox = document.getElementById("checkcheck") as HTMLInputElement; + checkbox.checked = false; + const searchFilter = document.getElementById("search-filter") as HTMLInputElement; + searchFilter.innerText = ""; + searchFilter.value = ""; + currentFilter?.classList.remove("using-filter"); + } + + // Update function for search and selected data + function update(filteredData: (Mentor | Student)[], filteredSelectData: (Mentor | Student)[]) { + setSearch(filteredData as Mentor[] | Student[]); + setSelected(filteredSelectData as Mentor[] | Student[]); + setAllSelected(false); + const checkbox = document.getElementById("checkcheck") as HTMLInputElement; + checkbox.checked = false; + } + + // Select specific checkbox + function selectCheckbox(id: number) { + const checkbox = document.getElementById(id + "check") as HTMLInputElement; + checkbox.checked = !checkbox.checked; + if (checkbox.checked) { + const selectedRow = searchData.find(row => row.id === id); + if (selectedRow) { + setSelected([...selectedData, selectedRow] as Mentor[] | Student[]); + } + } else { + setSelected(selectedData.filter(row => row.id !== id) as Mentor[] | Student[]); + } + } + + // Toggle for checkboxes + function toggleAllCheckboxes() { + if (allSelected) { + deselectAllCheckboxes(); + } else { + selectAllCheckboxes(); + } + setAllSelected(!allSelected); + } + + // Helper for toggle + function selectAllCheckboxes() { + const checkboxes = document.querySelectorAll("input[type=checkbox]"); + checkboxes.forEach(checkbox => { + (checkbox as HTMLInputElement).checked = true; + }); + setSelected(searchData); + } + // Helper for toggle + function deselectAllCheckboxes() { + const checkboxes = document.querySelectorAll("input[type=checkbox]"); + checkboxes.forEach(checkbox => { + (checkbox as HTMLInputElement).checked = false; + }); + setSelected([]); + } + + // Filter search for table + function filterSearch(event: React.ChangeEvent) { + const search = event.target.value.toLowerCase(); + if (search.length === 0) { + update(tableData, selectedData); + return; + } + const filteredData = tableData.filter(row => { + return row.name.toLowerCase().includes(search) || row.email.toLowerCase().includes(search); + }); + const filteredSelectedData = selectedData.filter(row => { + return row.name.toLowerCase().includes(search) || row.email.toLowerCase().includes(search); + }); + + if (currentFilter != null) { + currentFilter.classList.remove("using-filter"); + } + update(filteredData, filteredSelectedData); + } + + // Filter string + function filterString(event: React.MouseEvent, field: keyof Mentor | keyof Student) { + const filter = (event.target as HTMLButtonElement).innerText; + let filteredData: (Student | Mentor)[] = []; + let filteredSelectedData: (Student | Mentor)[] = []; + + filteredData = tableData.filter(row => { + if (field in row) { + const value = row[field as keyof typeof row]; + if (filter.includes("+")) { + return value != null ? value.toString() >= filter.slice(0, -1) : false; + } + return value != null ? value.toString().includes(filter) : false; + } + return false; + }); + filteredSelectedData = selectedData.filter(row => { + if (field in row) { + const value = row[field as keyof typeof row]; + if (filter.includes("+")) { + return value != null ? value.toString() >= filter.slice(0, -1) : false; + } + return value != null ? value.toString().includes(filter) : false; + } + return false; + }); + + checkFilter(event); + update(filteredData, filteredSelectedData); + } + + // Check filter + function checkFilter(event: React.MouseEvent) { + const mainButton = (event.target as HTMLButtonElement).parentElement?.previousElementSibling as HTMLButtonElement; + if (currentFilter != null && currentFilter != mainButton) { + currentFilter.classList.remove("using-filter"); + } + mainButton.classList.add("using-filter"); + setCurrentFilter(mainButton); + const searchFilter = document.getElementById("search-filter") as HTMLInputElement; + if (searchFilter) { + searchFilter.value = ""; + } + } + + // Reset filters + function resetFilters(event: React.MouseEvent) { + const resetButton = event.target as HTMLButtonElement; + if (currentFilter != null && currentFilter != resetButton) { + return; + } + update(tableData, selectedData); + resetButton.classList.remove("using-filter"); + setCurrentFilter(null); + } + + // Function for Copy Email + function copyEmail() { + const selected: string[] = []; + selectedData.forEach(userSelected => { + selected.push(userSelected["email"]); + }); + + const defaultCopy = document.getElementById("default-copy") as HTMLDivElement; + const successCopy = document.getElementById("success-copy") as HTMLDivElement; + + defaultCopy.classList.add("hidden"); + successCopy.classList.remove("hidden"); + + // reset to default state + setTimeout(() => { + defaultCopy.classList.remove("hidden"); + successCopy.classList.add("hidden"); + }, 2000); + + navigator.clipboard.writeText(selected.join(", ")); + } + + // Debugging Function for seeing selected data + // function getSelectedData() { + // console.log(selectedData); + // return selectedData; + // } + + return ( +
+
+
+ +
+
+ + {isStudents ? ( + + ) : ( + <> + + + + )} +
+ +
+ {isStudents ?
Students List
:
Mentors List
} + +
+ + + + + + + + {isStudents ? ( + <> + + + + + ) : ( + <> + + + + + )} + + + + {searchData.length === 0 ?
No data found...
: null} + + {searchData.map(row => ( + navigate(`/sections/${row.section}`)} + onClick={() => selectCheckbox(row.id)} + > + { + e.preventDefault(); + e.stopPropagation(); + }} + /> + {isStudents ? ( + <> + + + + + + + ) : ( + <> + + + + + + + )} + + ))} + +
NameEmailMentor NameTimeUnexcused AbsencesFamilyTimeSection Size
{row.name}{row.email}{(row as Student).mentorName}{row.dayTime}{(row as Student).numUnexcused}{row.name}{row.email}{(row as Mentor).family}{row.dayTime}{(row as Mentor).numStudents}
+
+
+ ); +} diff --git a/csm_web/frontend/src/components/coord_interface/DropBox.tsx b/csm_web/frontend/src/components/coord_interface/DropBox.tsx new file mode 100644 index 00000000..c6729100 --- /dev/null +++ b/csm_web/frontend/src/components/coord_interface/DropBox.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Mentor, Student } from "../../utils/queries/coord"; + +interface DropBoxProps { + items: Array; + name: string; + field: keyof Mentor | keyof Student; + func: (event: React.MouseEvent, field: keyof Mentor | keyof Student) => void; + reset: (event: React.MouseEvent) => void; +} + +export default function DropBox({ name, items, func, field, reset }: DropBoxProps) { + return ( +
+ +
+ {items.map(item => ( + + ))} +
+
+ ); +} diff --git a/csm_web/frontend/src/components/coord_interface/SearchBar.tsx b/csm_web/frontend/src/components/coord_interface/SearchBar.tsx new file mode 100644 index 00000000..90743a85 --- /dev/null +++ b/csm_web/frontend/src/components/coord_interface/SearchBar.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import SearchIcon from "../../../static/frontend/img/search.svg"; + +interface SearchBarProps { + refObject?: React.RefObject; + onChange?: React.ChangeEventHandler; +} + +export const SearchBar = ({ refObject, onChange }: SearchBarProps) => { + return ( +
+ + +
+ ); +}; diff --git a/csm_web/frontend/src/components/course/Course.tsx b/csm_web/frontend/src/components/course/Course.tsx index e1d20cbc..7282f1b6 100644 --- a/csm_web/frontend/src/components/course/Course.tsx +++ b/csm_web/frontend/src/components/course/Course.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon"; import React, { useState } from "react"; -import { useParams } from "react-router-dom"; +import { Link, useParams } from "react-router-dom"; import { DEFAULT_LONG_LOCALE_OPTIONS } from "../../utils/datetime"; import { useCourseSections } from "../../utils/queries/courses"; import { Course as CourseType } from "../../utils/types"; @@ -154,6 +154,10 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps): {userIsCoordinator && (
+ + + +