From c61aebc3f28d1e1ab465d2f0787a469fd1adbe66 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Fri, 22 Nov 2024 10:50:58 -0800 Subject: [PATCH 01/14] basic implementation --- frontend/src/components/Calendar/Calendar.tsx | 88 ++++++++++++ frontend/src/components/Calendar/Datebox.tsx | 30 ++++ frontend/src/components/Calendar/types.ts | 22 +++ frontend/src/components/Calendar/util.ts | 39 +++++ .../CalendarTable/CalendarTable.tsx | 136 ++++++++++++++++++ .../src/components/CalendarTable/Filters.tsx | 43 ++++++ .../src/components/CalendarTable/TBody.tsx | 45 ++++++ .../src/components/CalendarTable/THead.tsx | 84 +++++++++++ .../src/components/CalendarTable/types.ts | 12 ++ .../CalendarTable/useColumnSchema.tsx | 38 +++++ .../StudentsTable/useColumnSchema.tsx | 2 +- frontend/src/constants/navigation.tsx | 18 +++ frontend/src/pages/calendar.tsx | 56 ++++++++ 13 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/Calendar/Calendar.tsx create mode 100644 frontend/src/components/Calendar/Datebox.tsx create mode 100644 frontend/src/components/Calendar/types.ts create mode 100644 frontend/src/components/Calendar/util.ts create mode 100644 frontend/src/components/CalendarTable/CalendarTable.tsx create mode 100644 frontend/src/components/CalendarTable/Filters.tsx create mode 100644 frontend/src/components/CalendarTable/TBody.tsx create mode 100644 frontend/src/components/CalendarTable/THead.tsx create mode 100644 frontend/src/components/CalendarTable/types.ts create mode 100644 frontend/src/components/CalendarTable/useColumnSchema.tsx create mode 100644 frontend/src/pages/calendar.tsx diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx new file mode 100644 index 00000000..3dcbae53 --- /dev/null +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -0,0 +1,88 @@ +import { Poppins } from "next/font/google"; +import React, { useEffect, useState } from "react"; + +import { Datebox } from "./Datebox"; +import { Day, Months, Weekdays } from "./types"; +import { generateDates } from "./util"; + +const poppins = Poppins({ weight: ["400", "700"], style: "normal", subsets: [] }); + +// // +export const Calendar: React.FC = () => { + const today = new Date(); + + const [month, changeMonth] = useState(today.getMonth()); + const [year, changeYear] = useState(today.getFullYear()); + const [calendarHeader, changeCalendarHeader] = useState(Months[month] + " " + year); + const [dates, changeDates] = useState(generateDates(month, year)); + + useEffect(() => { + changeCalendarHeader(Months[month] + " " + year); + changeDates(generateDates(month, year)); + }, [month, year]); + + const decrementMonth = () => { + if (month === 0) { + changeMonth(11); + changeYear(year - 1); + } else { + changeMonth(month - 1); + } + }; + + const incrementMonth = () => { + if (month === 11) { + changeMonth(0); + changeYear(year + 1); + } else { + changeMonth(month + 1); + } + }; + + const bodyClass = `mx-auto w-full border rounded-lg shadow ${poppins.className}`; + + return ( +
+
+ {/* */} +
+

{calendarHeader}

+
+ + +
+
+
+
+ {Weekdays.slice(0, Weekdays.length - 1).map((day, i) => ( +
+ {" "} + {day}{" "} +
+ ))} +
{Weekdays[Weekdays.length - 1]}
+
+
+ {dates.slice(0, dates.length).map((date, i) => ( + + ))} +
+
+ ); +}; diff --git a/frontend/src/components/Calendar/Datebox.tsx b/frontend/src/components/Calendar/Datebox.tsx new file mode 100644 index 00000000..bd37c35a --- /dev/null +++ b/frontend/src/components/Calendar/Datebox.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export type DateboxProps = { + day: number; + hours: number; + saturday?: boolean; +}; + +export function Datebox({ day, hours, saturday }: DateboxProps) { + let boxClass = "border-r border-t p-2 flex flex-col items-center"; + if (saturday) { + boxClass = "border-t p-2 flex flex-col items-center"; + } + + return ( +
+
{day}
+ {hours !== 0 && ( + + )} + {hours === 0 && ( + + )} +
+ ); +} diff --git a/frontend/src/components/Calendar/types.ts b/frontend/src/components/Calendar/types.ts new file mode 100644 index 00000000..84e517aa --- /dev/null +++ b/frontend/src/components/Calendar/types.ts @@ -0,0 +1,22 @@ +export type Day = { + month: number; + year: number; + day: number; +}; + +export const Months: string[] = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +export const Weekdays: string[] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; diff --git a/frontend/src/components/Calendar/util.ts b/frontend/src/components/Calendar/util.ts new file mode 100644 index 00000000..8d90d563 --- /dev/null +++ b/frontend/src/components/Calendar/util.ts @@ -0,0 +1,39 @@ +import { Day } from "./types"; + +/** + * This function generates the dates for the calendar. + * 0 = Sunday, 1 = Monday, ..., 6 = Saturday + * 0 = Jan, 1 = Fed, ..., 11 = Dec + * @param month + * @param year + * @returns + */ +export const generateDates = (month: number, year: number): Day[] => { + const days: Day[] = []; + const first = new Date(year, month, 1); + const last = new Date(year, month + 1, 0); + + // get days before start of month + const startDay = first.getDay(); + + for (let i = 0; i < startDay; i++) { + const date = new Date(year, month, i - startDay + 1); + days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate() }); + } + + // get current month days + for (let day = 1; day <= last.getDate(); day++) { + const date = new Date(year, month, day).getDate(); + days.push({ month, year, day: date }); + } + + // get days after end of month + const endDay = last.getDay(); + const endDate = last.getDate(); + for (let i = endDay + 1; i <= 6; i++) { + const date = new Date(year, month, i - endDay + endDate); + days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate() }); + } + + return days; +}; diff --git a/frontend/src/components/CalendarTable/CalendarTable.tsx b/frontend/src/components/CalendarTable/CalendarTable.tsx new file mode 100644 index 00000000..d24ef1e6 --- /dev/null +++ b/frontend/src/components/CalendarTable/CalendarTable.tsx @@ -0,0 +1,136 @@ +import { + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import React, { useContext, useEffect, useMemo, useState } from "react"; + +import { getAllStudents } from "../../api/students"; +import LoadingSpinner from "../LoadingSpinner"; +import { fuzzyFilter, programFilterFn, statusFilterFn } from "../StudentsTable/FilterFns"; +import { StudentMap } from "../StudentsTable/types"; +import { Table } from "../ui/table"; + +// import Filter from "./Filters"; +import TBody from "./TBody"; +import THead from "./THead"; +import { CalendarTableRow } from "./types"; +import { useColumnSchema } from "./useColumnSchema"; + +import { ProgramsContext } from "@/contexts/program"; +// import { UserContext } from "@/contexts/user"; +import { useWindowSize } from "@/hooks/useWindowSize"; +import { cn } from "@/lib/utils"; + +export default function CalendarTable() { + const [allStudents, setAllStudents] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [calendarTable, setCalendarTable] = useState([]); + const [globalFilter, setGlobalFilter] = useState(""); + const { isTablet } = useWindowSize(); + + const { allPrograms } = useContext(ProgramsContext); + // const { isAdmin } = useContext(UserContext); + + // get all students and store them in allStudents + useEffect(() => { + getAllStudents().then( + (result) => { + console.log(result); + if (result.success) { + // Convert student array to object with keys as ids and values as corresponding student + const studentsObject = result.data.reduce((obj, student) => { + obj[student._id] = student; + return obj; + }, {} as StudentMap); + + setAllStudents(studentsObject); + setIsLoading(false); + } else { + console.log(result.error); + } + }, + (error) => { + console.log(error); + }, + ); + }, []); + + // Take all students and put them in rows for table + useEffect(() => { + const tableRows: CalendarTableRow[] = Object.values(allStudents).flatMap((student) => { + // Generate a row for each program the student is enrolled in + return student.programs.map( + (program) => + ({ + id: student._id, + profilePicture: "filler", + student: `${student.student.firstName} ${student.student.lastName}`, + programs: program, + }) as CalendarTableRow, + ); + }); + + setCalendarTable(tableRows); + }, [allStudents]); + + const columns = useColumnSchema({ allPrograms }); + const data = useMemo(() => calendarTable, [calendarTable]); + + // have to make different filter functions + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + filterFns: { + fuzzy: fuzzyFilter, + programFilter: programFilterFn, + statusFilter: statusFilterFn, + }, + state: { + globalFilter, + }, + onGlobalFilterChange: setGlobalFilter, + // globalFilterFn: fuzzyFilter, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + debugTable: true, + debugHeaders: true, + debugColumns: true, + }); + + return ( +
+
+

+ Calendar +

+ +

Choose a Student to View Attendance

+
+ {isLoading ? ( + + ) : ( + + + +
+ )} +
+ ); +} diff --git a/frontend/src/components/CalendarTable/Filters.tsx b/frontend/src/components/CalendarTable/Filters.tsx new file mode 100644 index 00000000..fb76e18c --- /dev/null +++ b/frontend/src/components/CalendarTable/Filters.tsx @@ -0,0 +1,43 @@ +import { Table } from "@tanstack/react-table"; +import React from "react"; + +import SearchIcon from "../../../public/icons/search.svg"; +import DebouncedInput from "../DebouncedInput"; +import { ProgramFilter } from "../StudentsTable/FilterFns"; + +import { CalendarTableRow } from "./types"; + +export default function Filter({ + globalFilter, + setGlobalFilter, + table, +}: { + globalFilter: string; + setGlobalFilter: React.Dispatch>; + table: Table; +}) { + return ( +
+ {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + if (!header.column.getCanFilter()) return null; + if (header.column.id === "Attendance") { + return ; + } + return null; + })} + } + value={globalFilter ?? ""} + onChange={(val) => { + setGlobalFilter(val); + }} + placeholder="Search in Students" + className="h-full min-w-[200px] p-0 px-2" + /> + + ))} +
+ ); +} diff --git a/frontend/src/components/CalendarTable/TBody.tsx b/frontend/src/components/CalendarTable/TBody.tsx new file mode 100644 index 00000000..96d01565 --- /dev/null +++ b/frontend/src/components/CalendarTable/TBody.tsx @@ -0,0 +1,45 @@ +import { Table, flexRender } from "@tanstack/react-table"; +import Image from "next/image"; + +import { TableBody, TableCell, TableRow } from "../ui/table"; + +import { CalendarTableRow } from "./types"; + +export default function TBody({ table }: { table: Table }) { + // If there are no students, display a placeholder + if (table.getRowModel().rows.length === 0) { + return ( + + + +
+ no students placeholder + + No Students +
+
+
+
+ ); + } + + return ( + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + ); +} diff --git a/frontend/src/components/CalendarTable/THead.tsx b/frontend/src/components/CalendarTable/THead.tsx new file mode 100644 index 00000000..2b5d2a63 --- /dev/null +++ b/frontend/src/components/CalendarTable/THead.tsx @@ -0,0 +1,84 @@ +import { HeaderGroup, Table, flexRender } from "@tanstack/react-table"; +import React from "react"; + +import SearchIcon from "../../../public/icons/search.svg"; +import DebouncedInput from "../DebouncedInput"; +import { TableHead, TableHeader, TableRow } from "../ui/table"; + +import { CalendarTableRow } from "./types"; + +export function TableActionsHeader({ + headerGroup, + globalFilter, + setGlobalFilter, +}: { + headerGroup: HeaderGroup; + globalFilter: string; + setGlobalFilter: React.Dispatch>; +}) { + return ( + + +
+ + {headerGroup.headers.map((header) => { + if (!header.column.getCanFilter()) return null; + return null; + })} + } + value={globalFilter ?? ""} + onChange={(val) => { + setGlobalFilter(val); + }} + placeholder="Search in Students" + className="h-full min-w-[200px] p-0 px-2" + /> + +
+
+
+ ); +} + +function TableDataHeader({ headerGroup }: { headerGroup: HeaderGroup }) { + return ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ); +} + +export default function THead({ + // globalFilter, + // setGlobalFilter, + table, +}: { + // globalFilter: string; + // setGlobalFilter: React.Dispatch>; + table: Table; +}) { + return ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {/* */} + + + ))} + + ); +} diff --git a/frontend/src/components/CalendarTable/types.ts b/frontend/src/components/CalendarTable/types.ts new file mode 100644 index 00000000..7995b946 --- /dev/null +++ b/frontend/src/components/CalendarTable/types.ts @@ -0,0 +1,12 @@ +import { ColumnDef } from "@tanstack/react-table"; + +import { ProgramLink } from "../StudentForm/types"; + +export type CalendarTableRow = { + id: string; + profilePicture: string; + student: string; + programs: ProgramLink; +}; + +export type Columns = ColumnDef[]; diff --git a/frontend/src/components/CalendarTable/useColumnSchema.tsx b/frontend/src/components/CalendarTable/useColumnSchema.tsx new file mode 100644 index 00000000..f1d4b833 --- /dev/null +++ b/frontend/src/components/CalendarTable/useColumnSchema.tsx @@ -0,0 +1,38 @@ +import { useMemo } from "react"; + +import { ProgramLink } from "../StudentForm/types"; +import { ProgramMap } from "../StudentsTable/types"; +import { ProgramPill } from "../StudentsTable/useColumnSchema"; + +import { Columns } from "./types"; + +export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { + const columns: Columns = [ + { + accessorKey: "profilePicture", + header: "Profile Picture", + cell: () => filler, + enableColumnFilter: false, + }, + { + accessorKey: "student", + header: "Student Name", + cell: (info) => ( + {info.getValue() as string} + ), + enableColumnFilter: false, + }, + { + accessorKey: "programs", + header: "Attendance", + cell: (info) => { + const programLink = info.getValue() as unknown as ProgramLink; + const link = programLink.programId; + const program = allPrograms[link]; + return ; + }, + }, + ]; + + return useMemo(() => columns, [allPrograms]); +} diff --git a/frontend/src/components/StudentsTable/useColumnSchema.tsx b/frontend/src/components/StudentsTable/useColumnSchema.tsx index ef6a4ade..36607062 100644 --- a/frontend/src/components/StudentsTable/useColumnSchema.tsx +++ b/frontend/src/components/StudentsTable/useColumnSchema.tsx @@ -14,7 +14,7 @@ import { cn } from "@/lib/utils"; const poppins = Poppins({ weight: ["400", "700"], style: "normal", subsets: [] }); -const ProgramPill = ({ name, color }: { name: string; color: string }) => { +export const ProgramPill = ({ name, color }: { name: string; color: string }) => { const { isTablet } = useWindowSize(); return ( diff --git a/frontend/src/constants/navigation.tsx b/frontend/src/constants/navigation.tsx index ce8f4ee2..ce7ebc3d 100644 --- a/frontend/src/constants/navigation.tsx +++ b/frontend/src/constants/navigation.tsx @@ -140,6 +140,24 @@ export const useNavigation = () => { ), }, + { + title: "Calendar", + href: "/calendar", + icon: ( + + + + ), + }, { title: "Attendance", href: "/attendance", diff --git a/frontend/src/pages/calendar.tsx b/frontend/src/pages/calendar.tsx new file mode 100644 index 00000000..fad3ed6e --- /dev/null +++ b/frontend/src/pages/calendar.tsx @@ -0,0 +1,56 @@ +import CalendarTable from "@/components/CalendarTable/CalendarTable"; +// import Back from '../../public/icons/back.svg' +// import { Calendar as C } from "@/components/Calendar/Calendar" +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; +// import { useWindowSize } from "@/hooks/useWindowSize"; +// import { useMemo } from "react"; + +export default function Calendar() { + useRedirectToLoginIfNotSignedIn(); + + // const { windowSize } = useWindowSize(); + // const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); + // const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); + // const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); + + // let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; + // let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; + // let titleClass = "font-[alternate-gothic]"; + // const backButton = "flex space-x-0 text-lg"; + + // if (isTablet) { + // titleClass += " text-2xl leading-none h-6"; + // mainClass += " p-0"; + + // if (isMobile) { + // headerClass += " pt-2 pb-3"; + // } else { + // headerClass += " p-2 py-4"; + // } + // } else { + // headerClass += "pt-10 pb-5"; + + // if (extraLarge) { + // headerClass += " max-w-[1740px]"; + // } else { + // headerClass += " max-w-[1160px]"; + // } + // } + + return ( + //
+ //
+ //
+ // + // {!isTablet &&

Student List

} + //
+ //

+ // Alice Anderson - UCI # 123456 + //

+ //
+ + // + //
+ + ); +} From b81d21ccb5eac83cb2cf3e87047c0f31870d27f1 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Fri, 22 Nov 2024 18:18:32 -0800 Subject: [PATCH 02/14] completed UI --- backend/src/controllers/student.ts | 17 +++ backend/src/routes/student.ts | 1 + frontend/src/api/students.ts | 10 ++ frontend/src/components/Calendar/Datebox.tsx | 4 +- .../CalendarTable/CalendarTable.tsx | 11 +- .../src/components/CalendarTable/Filters.tsx | 127 +++++++++++++++-- .../src/components/CalendarTable/types.ts | 8 +- .../CalendarTable/useColumnSchema.tsx | 38 ++++- frontend/src/pages/calendar.tsx | 130 +++++++++++------- 9 files changed, 270 insertions(+), 76 deletions(-) diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index ef5e965e..90335d99 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -5,6 +5,7 @@ import { RequestHandler } from "express"; import { validationResult } from "express-validator"; +import createHttpError from "http-errors"; import mongoose, { HydratedDocument } from "mongoose"; import EnrollmentModel from "../models/enrollment"; @@ -89,3 +90,19 @@ export const getAllStudents: RequestHandler = async (_, res, next) => { next(error); } }; + +export const getStudent: RequestHandler = async (req, res, next) => { + try { + const id = req.params.id; + + const student = await StudentModel.findById(id); + + if (student === null) { + throw createHttpError(404, "Student not found"); + } + + res.status(200).json(student); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts index a534d31f..713b23a0 100644 --- a/backend/src/routes/student.ts +++ b/backend/src/routes/student.ts @@ -12,5 +12,6 @@ const router = express.Router(); router.post("/create", StudentValidator.createStudent, StudentController.createStudent); router.put("/edit/:id", StudentValidator.editStudent, StudentController.editStudent); router.get("/all", StudentController.getAllStudents); +router.get("/:id", StudentController.getStudent); export default router; diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index 8a1583cc..369b2621 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -40,3 +40,13 @@ export async function getAllStudents(): Promise> { return handleAPIError(error); } } + +export async function getStudent(id: string): Promise> { + try { + const response = await GET(`/student/${id}`); + const json = (await response.json()) as Student; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/components/Calendar/Datebox.tsx b/frontend/src/components/Calendar/Datebox.tsx index bd37c35a..847fd59d 100644 --- a/frontend/src/components/Calendar/Datebox.tsx +++ b/frontend/src/components/Calendar/Datebox.tsx @@ -7,9 +7,9 @@ export type DateboxProps = { }; export function Datebox({ day, hours, saturday }: DateboxProps) { - let boxClass = "border-r border-t p-2 flex flex-col items-center"; + let boxClass = "border-r border-t p-4 flex flex-col items-center"; if (saturday) { - boxClass = "border-t p-2 flex flex-col items-center"; + boxClass = "border-t p-4 flex flex-col items-center"; } return ( diff --git a/frontend/src/components/CalendarTable/CalendarTable.tsx b/frontend/src/components/CalendarTable/CalendarTable.tsx index d24ef1e6..87147306 100644 --- a/frontend/src/components/CalendarTable/CalendarTable.tsx +++ b/frontend/src/components/CalendarTable/CalendarTable.tsx @@ -10,10 +10,11 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import { getAllStudents } from "../../api/students"; import LoadingSpinner from "../LoadingSpinner"; -import { fuzzyFilter, programFilterFn, statusFilterFn } from "../StudentsTable/FilterFns"; import { StudentMap } from "../StudentsTable/types"; import { Table } from "../ui/table"; +import Filter, { fuzzyFilter, programFilterFn, statusFilterFn } from "./Filters"; + // import Filter from "./Filters"; import TBody from "./TBody"; import THead from "./THead"; @@ -67,9 +68,9 @@ export default function CalendarTable() { (program) => ({ id: student._id, - profilePicture: "filler", + profilePicture: "default", student: `${student.student.firstName} ${student.student.lastName}`, - programs: program, + programs: { ...program, studentId: student._id }, }) as CalendarTableRow, ); }); @@ -94,7 +95,7 @@ export default function CalendarTable() { globalFilter, }, onGlobalFilterChange: setGlobalFilter, - // globalFilterFn: fuzzyFilter, + globalFilterFn: fuzzyFilter, getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), @@ -117,6 +118,8 @@ export default function CalendarTable() {

Choose a Student to View Attendance

+ + {isLoading ? ( diff --git a/frontend/src/components/CalendarTable/Filters.tsx b/frontend/src/components/CalendarTable/Filters.tsx index fb76e18c..b6a23767 100644 --- a/frontend/src/components/CalendarTable/Filters.tsx +++ b/frontend/src/components/CalendarTable/Filters.tsx @@ -1,12 +1,101 @@ -import { Table } from "@tanstack/react-table"; +import { RankingInfo, rankItem } from "@tanstack/match-sorter-utils"; +import { FilterFn, Table } from "@tanstack/react-table"; import React from "react"; import SearchIcon from "../../../public/icons/search.svg"; import DebouncedInput from "../DebouncedInput"; +import { ProgramLink } from "../StudentForm/types"; import { ProgramFilter } from "../StudentsTable/FilterFns"; import { CalendarTableRow } from "./types"; +// Extend the FilterFns and FilterMeta interfaces +/* eslint-disable */ +declare module "@tanstack/table-core" { + interface FilterFns { + fuzzy: FilterFn; + programFilter: FilterFn; + statusFilter: FilterFn; + } + interface FilterMeta { + itemRank: RankingInfo; + } +} +/* eslint-enable */ + +export const programFilterFn: FilterFn = (rows, id, filterValue) => { + if (filterValue === "") return true; // no filter case + let containsProgram = false; + const programLinks: ProgramLink[] = rows.getValue(id); + programLinks.forEach((prog) => { + if (prog.programId === filterValue && prog.status === "Joined") { + containsProgram = true; + } + }); + return containsProgram; +}; + +export const statusFilterFn: FilterFn = (rows, id, filterValue) => { + if (filterValue === "") return true; // no filter case + let containsStatus = false; + const programLinks: ProgramLink[] = rows.getValue(id); + programLinks.forEach((prog) => { + if (prog.status === filterValue) { + containsStatus = true; + } + }); + return containsStatus; +}; + +// Filter function from tanstack docs for global filter (search in students ) +export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value as string); + + // Store the itemRank info + addMeta({ + itemRank, + }); + + // Return if the item should be filtered in/out + return itemRank.passed; +}; + +// export default function Filter({ +// globalFilter, +// setGlobalFilter, +// table, +// }: { +// globalFilter: string; +// setGlobalFilter: React.Dispatch>; +// table: Table; +// }) { +// return ( +//
+// {table.getHeaderGroups().map((headerGroup) => ( +// +// } +// value={globalFilter ?? ""} +// onChange={(val) => { +// setGlobalFilter(val); +// }} +// placeholder="Search in Students" +// className="h-full min-w-[300px] p-0 px-2 border border-gray-300 rounded-md bg-white" +// /> +// {headerGroup.headers.map((header) => { +// if (!header.column.getCanFilter()) return null; +// if (header.column.id === "programs") { +// return ; +// } +// return null; +// })} +// +// ))} +//
+// ); +// } + export default function Filter({ globalFilter, setGlobalFilter, @@ -17,25 +106,35 @@ export default function Filter({ table: Table; }) { return ( -
+
+ {/* Global Search Filter */} +
+ + { + setGlobalFilter(val); + }} + placeholder="Search in Students" + className="w-full border-none text-gray-600 outline-none" + /> +
+ + {/* Program Filter */} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { - if (!header.column.getCanFilter()) return null; - if (header.column.id === "Attendance") { - return ; + if (header.column.id === "programs" && header.column.getCanFilter()) { + return ( + + ); } return null; })} - } - value={globalFilter ?? ""} - onChange={(val) => { - setGlobalFilter(val); - }} - placeholder="Search in Students" - className="h-full min-w-[200px] p-0 px-2" - /> ))}
diff --git a/frontend/src/components/CalendarTable/types.ts b/frontend/src/components/CalendarTable/types.ts index 7995b946..aa03505f 100644 --- a/frontend/src/components/CalendarTable/types.ts +++ b/frontend/src/components/CalendarTable/types.ts @@ -2,11 +2,15 @@ import { ColumnDef } from "@tanstack/react-table"; import { ProgramLink } from "../StudentForm/types"; +export type EnrollmentLink = { + studentId: string; +} & ProgramLink; + export type CalendarTableRow = { id: string; profilePicture: string; student: string; - programs: ProgramLink; + programs: EnrollmentLink; }; -export type Columns = ColumnDef[]; +export type Columns = ColumnDef[]; diff --git a/frontend/src/components/CalendarTable/useColumnSchema.tsx b/frontend/src/components/CalendarTable/useColumnSchema.tsx index f1d4b833..913fbdff 100644 --- a/frontend/src/components/CalendarTable/useColumnSchema.tsx +++ b/frontend/src/components/CalendarTable/useColumnSchema.tsx @@ -1,17 +1,28 @@ +import Image from "next/image"; +import Link from "next/link"; import { useMemo } from "react"; -import { ProgramLink } from "../StudentForm/types"; import { ProgramMap } from "../StudentsTable/types"; import { ProgramPill } from "../StudentsTable/useColumnSchema"; -import { Columns } from "./types"; +import { Columns, EnrollmentLink } from "./types"; export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { const columns: Columns = [ { accessorKey: "profilePicture", header: "Profile Picture", - cell: () => filler, + cell: (info) => ( +
+ Profile Picture +
+ ), enableColumnFilter: false, }, { @@ -26,10 +37,23 @@ export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { accessorKey: "programs", header: "Attendance", cell: (info) => { - const programLink = info.getValue() as unknown as ProgramLink; - const link = programLink.programId; - const program = allPrograms[link]; - return ; + const enrollmentLink = info.getValue() as unknown as EnrollmentLink; + const studentId = enrollmentLink.studentId; + const programId = enrollmentLink.programId; + const program = allPrograms[programId]; + return ( + + + + ); }, }, ]; diff --git a/frontend/src/pages/calendar.tsx b/frontend/src/pages/calendar.tsx index fad3ed6e..48aa85b2 100644 --- a/frontend/src/pages/calendar.tsx +++ b/frontend/src/pages/calendar.tsx @@ -1,56 +1,92 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useMemo, useState } from "react"; + +import Back from "../../public/icons/back.svg"; + +import { Student, getStudent } from "@/api/students"; +import { Calendar } from "@/components/Calendar/Calendar"; import CalendarTable from "@/components/CalendarTable/CalendarTable"; -// import Back from '../../public/icons/back.svg' -// import { Calendar as C } from "@/components/Calendar/Calendar" +import LoadingSpinner from "@/components/LoadingSpinner"; import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; -// import { useWindowSize } from "@/hooks/useWindowSize"; -// import { useMemo } from "react"; +import { useWindowSize } from "@/hooks/useWindowSize"; -export default function Calendar() { +export default function Component() { useRedirectToLoginIfNotSignedIn(); - // const { windowSize } = useWindowSize(); - // const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); - // const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); - // const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); - - // let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; - // let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; - // let titleClass = "font-[alternate-gothic]"; - // const backButton = "flex space-x-0 text-lg"; - - // if (isTablet) { - // titleClass += " text-2xl leading-none h-6"; - // mainClass += " p-0"; - - // if (isMobile) { - // headerClass += " pt-2 pb-3"; - // } else { - // headerClass += " p-2 py-4"; - // } - // } else { - // headerClass += "pt-10 pb-5"; - - // if (extraLarge) { - // headerClass += " max-w-[1740px]"; - // } else { - // headerClass += " max-w-[1160px]"; - // } - // } + const router = useRouter(); + const { student, program } = router.query; + + const [currStudent, setStudent] = useState(); + // const [currProgram, setProgram ] = useState(); + const [isLoading, setIsLoading] = useState(true); + + const { windowSize } = useWindowSize(); + const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); + const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); + const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); + + if (!student || !program) return ; + + getStudent(student as string).then( + (result) => { + if (result.success) { + setStudent(result.data); + setIsLoading(false); + } else { + console.log(result.error); + } + }, + (error) => { + console.log(error); + }, + ); + + let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; + let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; + let titleClass = "font-[alternate-gothic]"; + const backButton = "flex space-x-0 text-lg"; + + if (isTablet) { + titleClass += " text-2xl leading-none h-6"; + mainClass += " p-0"; + + if (isMobile) { + headerClass += " pt-2 pb-3"; + } else { + headerClass += " p-2 py-4"; + } + } else { + headerClass += "pt-10 pb-5"; + + if (extraLarge) { + headerClass += " max-w-[1740px]"; + } else { + headerClass += " max-w-[1160px]"; + } + } return ( - //
- //
- //
- // - // {!isTablet &&

Student List

} - //
- //

- // Alice Anderson - UCI # 123456 - //

- //
- - // - //
- +
+ {isLoading ? ( + + ) : ( +
+
+
+ + + + {!isTablet &&

Student List

} +
+

+ {currStudent?.student.firstName + " " + currStudent?.student.lastName} - UCI # + 123456789 +

+
+ +
+ )} +
); } From 64411cce15da86a48cad97fd37d6911e2b511f44 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Thu, 5 Dec 2024 11:48:45 -0800 Subject: [PATCH 03/14] basic backend connection --- backend/src/controllers/calendar.ts | 77 +++++++++ backend/src/controllers/student.ts | 2 +- .../src/controllers/types/calendarTypes.ts | 9 + backend/src/routes/api.ts | 2 + backend/src/routes/calendar.ts | 15 ++ backend/src/routes/student.ts | 2 +- frontend/src/api/calendar.ts | 28 ++++ frontend/src/api/progressNotes.ts | 14 +- frontend/src/api/requests.ts | 4 + frontend/src/components/Calendar/Calendar.tsx | 156 ++++++++++-------- .../src/components/Calendar/CalendarBody.tsx | 94 +++++++++++ frontend/src/components/Calendar/types.ts | 1 + frontend/src/components/Calendar/util.ts | 36 +++- frontend/src/pages/calendar.tsx | 80 +-------- 14 files changed, 359 insertions(+), 161 deletions(-) create mode 100644 backend/src/controllers/calendar.ts create mode 100644 backend/src/controllers/types/calendarTypes.ts create mode 100644 backend/src/routes/calendar.ts create mode 100644 frontend/src/api/calendar.ts create mode 100644 frontend/src/components/Calendar/CalendarBody.tsx diff --git a/backend/src/controllers/calendar.ts b/backend/src/controllers/calendar.ts new file mode 100644 index 00000000..8bf93bf5 --- /dev/null +++ b/backend/src/controllers/calendar.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +/** + * Function handlers for calendar route requests + */ +import { RequestHandler } from "express"; +import createHttpError from "http-errors"; + +import EnrollmentModel from "../models/enrollment"; +import SessionModel from "../models/session"; + +import { Calendar } from "./types/calendarTypes"; + +/** + * Calendar Body: { + * + * studentId: string; + * programId: string; + * calendar: { + * date: Date; + * hours: number; + * session: string; + * }[] + * + * } + */ + +/** + * Request handler for getting all possible calendars + * @param req + * @param res + * @param next + */ +// export const getCalendars: RequestHandler = async (req, res, next) => { + +// } + +/** + * Request handler for getting calendar for student in program + * @param req + * @param res + * @param next + */ +export const getCalendar: RequestHandler = async (req, res, next) => { + try { + console.log(getCalendar); + const studentId = req.params.studentId; + const programId = req.params.programId; + + const enrollment = EnrollmentModel.find({ studentId, programId }); + if (!enrollment) { + throw createHttpError(404, "Enrollment not found"); + } + + // get all sessions with studentId and programId + const sessions = await SessionModel.find({ programId }); + + const calendar: Calendar = { studentId, programId, calendar: [] }; + for (const session of sessions) { + for (const student of session.students) { + if (student.studentId.toString() === studentId) { + let hours = 0; + if (session.marked) { + hours = student.hoursAttended; + } + const date = session.date; + const sessionId = session._id.toString(); + calendar.calendar.push({ date, hours, session: sessionId }); + } + } + } + console.log(calendar); + + return res.status(200).send(calendar); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index 90335d99..6472e6b0 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ /** - * Functions that process task route requests. + * Functions that process student route requests. */ import { RequestHandler } from "express"; diff --git a/backend/src/controllers/types/calendarTypes.ts b/backend/src/controllers/types/calendarTypes.ts new file mode 100644 index 00000000..fdf2badf --- /dev/null +++ b/backend/src/controllers/types/calendarTypes.ts @@ -0,0 +1,9 @@ +export type Calendar = { + studentId: string; + programId: string; + calendar: { + date: Date; + hours: number; + session: string; + }[]; +}; diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts index 80fbd758..05be9288 100644 --- a/backend/src/routes/api.ts +++ b/backend/src/routes/api.ts @@ -1,5 +1,6 @@ import express from "express"; +import calendarRouter from "./calendar"; import programRoutes from "./program"; import progressNoteRoutes from "./progressNote"; import sessionRoutes from "./session"; @@ -14,5 +15,6 @@ router.use("/student", studentRoutes); router.use("/program", programRoutes); router.use("/session", sessionRoutes); router.use("/progressNote", progressNoteRoutes); +router.use("/calendar", calendarRouter); export default router; diff --git a/backend/src/routes/calendar.ts b/backend/src/routes/calendar.ts new file mode 100644 index 00000000..a53ae2b0 --- /dev/null +++ b/backend/src/routes/calendar.ts @@ -0,0 +1,15 @@ +/** + * Calendar route requests + */ +import express from "express"; + +import * as CalendarController from "../controllers/calendar"; +import { verifyAuthToken } from "../validators/auth"; + +const router = express.Router(); + +router.get("/all", [verifyAuthToken]); +router.get("/:studentId/:programId", [verifyAuthToken], CalendarController.getCalendar); +router.put("/:studentId/:programId", [verifyAuthToken]); + +export default router; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts index 713b23a0..952cb84a 100644 --- a/backend/src/routes/student.ts +++ b/backend/src/routes/student.ts @@ -1,5 +1,5 @@ /** - * Task route requests. + * Student route requests. */ import express from "express"; diff --git a/frontend/src/api/calendar.ts b/frontend/src/api/calendar.ts new file mode 100644 index 00000000..258cbcac --- /dev/null +++ b/frontend/src/api/calendar.ts @@ -0,0 +1,28 @@ +import { GET, createAuthHeader, handleAPIError } from "../api/requests"; + +import type { APIResult } from "../api/requests"; + +export type CalendarResponse = { + studentId: string; + programId: string; + calendar: { + date: Date; + hours: number; + session: string; + }[]; +}; + +export async function getCalendar( + studentId: string, + programId: string, + firebaseToken: string, +): Promise> { + try { + const headers = createAuthHeader(firebaseToken); + const response = await GET(`/calendar/${studentId}/${programId}`, headers); + const json = (await response.json()) as CalendarResponse; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/api/progressNotes.ts b/frontend/src/api/progressNotes.ts index 9a9c45dc..92672000 100644 --- a/frontend/src/api/progressNotes.ts +++ b/frontend/src/api/progressNotes.ts @@ -1,10 +1,14 @@ -import { APIResult, DELETE, GET, POST, PUT, handleAPIError } from "@/api/requests"; +import { + APIResult, + DELETE, + GET, + POST, + PUT, + createAuthHeader, + handleAPIError, +} from "@/api/requests"; import { ProgressNote } from "@/components/ProgressNotes/types"; -export const createAuthHeader = (firebaseToken: string) => ({ - Authorization: `Bearer ${firebaseToken}`, -}); - export async function createProgressNote( studentId: string, dateLastUpdated: Date, diff --git a/frontend/src/api/requests.ts b/frontend/src/api/requests.ts index 534285be..6be48936 100644 --- a/frontend/src/api/requests.ts +++ b/frontend/src/api/requests.ts @@ -192,3 +192,7 @@ export function handleAPIError(error: unknown): APIError { } return { success: false, error: `Unknown error; ${String(error)}` }; } + +export const createAuthHeader = (firebaseToken: string) => ({ + Authorization: `Bearer ${firebaseToken}`, +}); diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx index 3dcbae53..86fa6854 100644 --- a/frontend/src/components/Calendar/Calendar.tsx +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -1,88 +1,102 @@ -import { Poppins } from "next/font/google"; -import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import { useContext, useEffect, useMemo, useState } from "react"; -import { Datebox } from "./Datebox"; -import { Day, Months, Weekdays } from "./types"; -import { generateDates } from "./util"; +import Back from "../../../public/icons/back.svg"; -const poppins = Poppins({ weight: ["400", "700"], style: "normal", subsets: [] }); +import { CalendarResponse, getCalendar } from "@/api/calendar"; +import { Student, getStudent } from "@/api/students"; +import { CalendarBody } from "@/components/Calendar/CalendarBody"; +import LoadingSpinner from "@/components/LoadingSpinner"; +import { UserContext } from "@/contexts/user"; +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; +import { useWindowSize } from "@/hooks/useWindowSize"; -// // -export const Calendar: React.FC = () => { - const today = new Date(); +export type CalendarProps = { + studentId: string; + programId: string; +}; + +export default function Calendar({ studentId, programId }: CalendarProps) { + useRedirectToLoginIfNotSignedIn(); + + const { firebaseUser } = useContext(UserContext); - const [month, changeMonth] = useState(today.getMonth()); - const [year, changeYear] = useState(today.getFullYear()); - const [calendarHeader, changeCalendarHeader] = useState(Months[month] + " " + year); - const [dates, changeDates] = useState(generateDates(month, year)); + const [currStudent, setStudent] = useState(); + // const [currProgram, setProgram ] = useState(); + const [calendar, setCalendar] = useState(); + const [isLoading, setIsLoading] = useState(true); + + const { windowSize } = useWindowSize(); + const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); + const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); + const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); useEffect(() => { - changeCalendarHeader(Months[month] + " " + year); - changeDates(generateDates(month, year)); - }, [month, year]); + if (firebaseUser) { + firebaseUser + ?.getIdToken() + .then(async (token) => { + const calendarResponse = await getCalendar(studentId, programId, token); + if (calendarResponse.success) { + setCalendar(calendarResponse.data); + } + const studentResponse = await getStudent(studentId); + if (studentResponse.success) { + setStudent(studentResponse.data); + setIsLoading(false); + } + }) + .catch((error) => { + console.error(error); + }); + } + }, [firebaseUser]); - const decrementMonth = () => { - if (month === 0) { - changeMonth(11); - changeYear(year - 1); + let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; + let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; + let titleClass = "font-[alternate-gothic]"; + const backButton = "flex space-x-0 text-lg"; + + if (isTablet) { + titleClass += " text-2xl leading-none h-6"; + mainClass += " p-0"; + + if (isMobile) { + headerClass += " pt-2 pb-3"; } else { - changeMonth(month - 1); + headerClass += " p-2 py-4"; } - }; + } else { + headerClass += "pt-10 pb-5"; - const incrementMonth = () => { - if (month === 11) { - changeMonth(0); - changeYear(year + 1); + if (extraLarge) { + headerClass += " max-w-[1740px]"; } else { - changeMonth(month + 1); + headerClass += " max-w-[1160px]"; } - }; - - const bodyClass = `mx-auto w-full border rounded-lg shadow ${poppins.className}`; + } return ( -
-
- {/* */} -
-

{calendarHeader}

-
- - +
+ {isLoading ? ( + + ) : ( +
+
+
+ + + + {!isTablet &&

Student List

} +
+

+ {currStudent?.student.firstName + " " + currStudent?.student.lastName} - UCI # + 123456789 +

-
-
-
- {Weekdays.slice(0, Weekdays.length - 1).map((day, i) => ( -
- {" "} - {day}{" "} -
- ))} -
{Weekdays[Weekdays.length - 1]}
-
-
- {dates.slice(0, dates.length).map((date, i) => ( - - ))} -
+ + + )}
); -}; +} diff --git a/frontend/src/components/Calendar/CalendarBody.tsx b/frontend/src/components/Calendar/CalendarBody.tsx new file mode 100644 index 00000000..1659a266 --- /dev/null +++ b/frontend/src/components/Calendar/CalendarBody.tsx @@ -0,0 +1,94 @@ +import { Poppins } from "next/font/google"; +import React, { useEffect, useState } from "react"; + +import { Datebox } from "./Datebox"; +import { Day, Months, Weekdays } from "./types"; +import { generateDates } from "./util"; + +import { CalendarResponse } from "@/api/calendar"; + +const poppins = Poppins({ weight: ["400", "700"], style: "normal", subsets: [] }); + +export type CalendarBodyProps = { + calendar?: CalendarResponse; +}; + +// +export const CalendarBody: React.FC = ({ calendar }: CalendarBodyProps) => { + const today = new Date(); + + const [month, changeMonth] = useState(today.getMonth()); + const [year, changeYear] = useState(today.getFullYear()); + const [calendarHeader, changeCalendarHeader] = useState(Months[month] + " " + year); + const [dates, changeDates] = useState(generateDates(month, year, calendar)); + + useEffect(() => { + changeCalendarHeader(Months[month] + " " + year); + changeDates(generateDates(month, year)); + }, [month, year]); + + const decrementMonth = () => { + if (month === 0) { + changeMonth(11); + changeYear(year - 1); + } else { + changeMonth(month - 1); + } + }; + + const incrementMonth = () => { + if (month === 11) { + changeMonth(0); + changeYear(year + 1); + } else { + changeMonth(month + 1); + } + }; + + const bodyClass = `mx-auto w-full border rounded-lg shadow ${poppins.className}`; + + return ( +
+
+ {/* */} +
+

{calendarHeader}

+
+ + +
+
+
+
+ {Weekdays.slice(0, Weekdays.length - 1).map((day, i) => ( +
+ {" "} + {day}{" "} +
+ ))} +
{Weekdays[Weekdays.length - 1]}
+
+
+ {dates.slice(0, dates.length).map((date, i) => ( + + ))} +
+
+ ); +}; diff --git a/frontend/src/components/Calendar/types.ts b/frontend/src/components/Calendar/types.ts index 84e517aa..54b7dd57 100644 --- a/frontend/src/components/Calendar/types.ts +++ b/frontend/src/components/Calendar/types.ts @@ -2,6 +2,7 @@ export type Day = { month: number; year: number; day: number; + hours: number; }; export const Months: string[] = [ diff --git a/frontend/src/components/Calendar/util.ts b/frontend/src/components/Calendar/util.ts index 8d90d563..83db774b 100644 --- a/frontend/src/components/Calendar/util.ts +++ b/frontend/src/components/Calendar/util.ts @@ -1,5 +1,7 @@ import { Day } from "./types"; +import { CalendarResponse } from "@/api/calendar"; + /** * This function generates the dates for the calendar. * 0 = Sunday, 1 = Monday, ..., 6 = Saturday @@ -8,7 +10,7 @@ import { Day } from "./types"; * @param year * @returns */ -export const generateDates = (month: number, year: number): Day[] => { +export const generateDates = (month: number, year: number, calendar?: CalendarResponse): Day[] => { const days: Day[] = []; const first = new Date(year, month, 1); const last = new Date(year, month + 1, 0); @@ -18,13 +20,29 @@ export const generateDates = (month: number, year: number): Day[] => { for (let i = 0; i < startDay; i++) { const date = new Date(year, month, i - startDay + 1); - days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate() }); + let hours = 0; + if (calendar) { + for (const c of calendar.calendar) { + if (c.date === date) { + hours = c.hours; + } + } + } + days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); } // get current month days for (let day = 1; day <= last.getDate(); day++) { - const date = new Date(year, month, day).getDate(); - days.push({ month, year, day: date }); + const date = new Date(year, month, day); + let hours = 0; + if (calendar) { + for (const c of calendar.calendar) { + if (c.date === date) { + hours = c.hours; + } + } + } + days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); } // get days after end of month @@ -32,7 +50,15 @@ export const generateDates = (month: number, year: number): Day[] => { const endDate = last.getDate(); for (let i = endDay + 1; i <= 6; i++) { const date = new Date(year, month, i - endDay + endDate); - days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate() }); + let hours = 0; + if (calendar) { + for (const c of calendar.calendar) { + if (c.date === date) { + hours = c.hours; + } + } + } + days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); } return days; diff --git a/frontend/src/pages/calendar.tsx b/frontend/src/pages/calendar.tsx index 48aa85b2..7178689b 100644 --- a/frontend/src/pages/calendar.tsx +++ b/frontend/src/pages/calendar.tsx @@ -1,15 +1,8 @@ -import Link from "next/link"; import { useRouter } from "next/router"; -import { useMemo, useState } from "react"; -import Back from "../../public/icons/back.svg"; - -import { Student, getStudent } from "@/api/students"; -import { Calendar } from "@/components/Calendar/Calendar"; +import Calendar from "@/components/Calendar/Calendar"; import CalendarTable from "@/components/CalendarTable/CalendarTable"; -import LoadingSpinner from "@/components/LoadingSpinner"; import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; -import { useWindowSize } from "@/hooks/useWindowSize"; export default function Component() { useRedirectToLoginIfNotSignedIn(); @@ -17,76 +10,7 @@ export default function Component() { const router = useRouter(); const { student, program } = router.query; - const [currStudent, setStudent] = useState(); - // const [currProgram, setProgram ] = useState(); - const [isLoading, setIsLoading] = useState(true); - - const { windowSize } = useWindowSize(); - const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); - const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); - const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); - if (!student || !program) return ; - getStudent(student as string).then( - (result) => { - if (result.success) { - setStudent(result.data); - setIsLoading(false); - } else { - console.log(result.error); - } - }, - (error) => { - console.log(error); - }, - ); - - let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; - let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; - let titleClass = "font-[alternate-gothic]"; - const backButton = "flex space-x-0 text-lg"; - - if (isTablet) { - titleClass += " text-2xl leading-none h-6"; - mainClass += " p-0"; - - if (isMobile) { - headerClass += " pt-2 pb-3"; - } else { - headerClass += " p-2 py-4"; - } - } else { - headerClass += "pt-10 pb-5"; - - if (extraLarge) { - headerClass += " max-w-[1740px]"; - } else { - headerClass += " max-w-[1160px]"; - } - } - - return ( -
- {isLoading ? ( - - ) : ( -
-
-
- - - - {!isTablet &&

Student List

} -
-

- {currStudent?.student.firstName + " " + currStudent?.student.lastName} - UCI # - 123456789 -

-
- -
- )} -
- ); + return ; } From cf67a3b8cecfcb91e285ef0f74b03fbc9ee65b90 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Sat, 4 Jan 2025 17:36:32 -0800 Subject: [PATCH 04/14] view calendar --- backend/src/controllers/calendar.ts | 1 - .../src/components/Calendar/CalendarBody.tsx | 2 +- frontend/src/components/Calendar/util.ts | 21 ++++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/calendar.ts b/backend/src/controllers/calendar.ts index 8bf93bf5..f9143cbc 100644 --- a/backend/src/controllers/calendar.ts +++ b/backend/src/controllers/calendar.ts @@ -42,7 +42,6 @@ import { Calendar } from "./types/calendarTypes"; */ export const getCalendar: RequestHandler = async (req, res, next) => { try { - console.log(getCalendar); const studentId = req.params.studentId; const programId = req.params.programId; diff --git a/frontend/src/components/Calendar/CalendarBody.tsx b/frontend/src/components/Calendar/CalendarBody.tsx index 1659a266..0e175976 100644 --- a/frontend/src/components/Calendar/CalendarBody.tsx +++ b/frontend/src/components/Calendar/CalendarBody.tsx @@ -24,7 +24,7 @@ export const CalendarBody: React.FC = ({ calendar }: Calendar useEffect(() => { changeCalendarHeader(Months[month] + " " + year); - changeDates(generateDates(month, year)); + changeDates(generateDates(month, year, calendar)); }, [month, year]); const decrementMonth = () => { diff --git a/frontend/src/components/Calendar/util.ts b/frontend/src/components/Calendar/util.ts index 83db774b..f40c32d0 100644 --- a/frontend/src/components/Calendar/util.ts +++ b/frontend/src/components/Calendar/util.ts @@ -23,7 +23,12 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe let hours = 0; if (calendar) { for (const c of calendar.calendar) { - if (c.date === date) { + const calendarDate = new Date(c.date); + if ( + calendarDate.getMonth() === date.getMonth() && + calendarDate.getDate() === date.getDate() && + calendarDate.getFullYear() === date.getFullYear() + ) { hours = c.hours; } } @@ -37,7 +42,12 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe let hours = 0; if (calendar) { for (const c of calendar.calendar) { - if (c.date === date) { + const calendarDate = new Date(c.date); + if ( + calendarDate.getMonth() === date.getMonth() && + calendarDate.getDate() === date.getDate() && + calendarDate.getFullYear() === date.getFullYear() + ) { hours = c.hours; } } @@ -53,7 +63,12 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe let hours = 0; if (calendar) { for (const c of calendar.calendar) { - if (c.date === date) { + const calendarDate = new Date(c.date); + if ( + calendarDate.getMonth() === date.getMonth() && + calendarDate.getDate() === date.getDate() && + calendarDate.getFullYear() === date.getFullYear() + ) { hours = c.hours; } } From 257665c710345251648816dab94794d11153e01c Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Thu, 9 Jan 2025 16:21:39 -0800 Subject: [PATCH 05/14] preliminary calendar complete --- backend/src/controllers/calendar.ts | 62 +++++++++++++++---- backend/src/controllers/session.ts | 12 ++++ .../src/controllers/types/calendarTypes.ts | 12 ++-- backend/src/routes/calendar.ts | 9 ++- backend/src/validators/calendar.ts | 33 ++++++++++ frontend/src/api/calendar.ts | 20 +++++- frontend/src/components/Calendar/Calendar.tsx | 12 +++- .../src/components/Calendar/CalendarBody.tsx | 14 ++++- frontend/src/components/Calendar/Datebox.tsx | 17 ++++- frontend/src/components/Calendar/types.ts | 1 + frontend/src/components/Calendar/util.ts | 36 +++++++++-- 11 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 backend/src/validators/calendar.ts diff --git a/backend/src/controllers/calendar.ts b/backend/src/controllers/calendar.ts index f9143cbc..1d0a6af5 100644 --- a/backend/src/controllers/calendar.ts +++ b/backend/src/controllers/calendar.ts @@ -8,7 +8,7 @@ import createHttpError from "http-errors"; import EnrollmentModel from "../models/enrollment"; import SessionModel from "../models/session"; -import { Calendar } from "./types/calendarTypes"; +import { Calendar, CalendarSlot } from "./types/calendarTypes"; /** * Calendar Body: { @@ -24,16 +24,6 @@ import { Calendar } from "./types/calendarTypes"; * } */ -/** - * Request handler for getting all possible calendars - * @param req - * @param res - * @param next - */ -// export const getCalendars: RequestHandler = async (req, res, next) => { - -// } - /** * Request handler for getting calendar for student in program * @param req @@ -74,3 +64,53 @@ export const getCalendar: RequestHandler = async (req, res, next) => { next(error); } }; + +/** + * Handler for editing a day in a calendar + * @param req + * @param res + * @param next + */ +export const editCalendar: RequestHandler = async (req, res, next) => { + try { + const studentId = req.params.studentId; + const programId = req.params.programId; + + const enrollment = await EnrollmentModel.findOne({ studentId, programId }); + if (!enrollment) { + throw createHttpError(404, "Enrollment not found"); + } + + const { hours, session } = req.body as CalendarSlot; + + const sessionObject = await SessionModel.findById(session); + + if (!sessionObject) { + throw createHttpError(404, "Session not found"); + } + + if (sessionObject.programId.toString() !== programId) { + throw createHttpError(404, "Incorrect program for session"); + } + + const student = sessionObject.students.find((s) => s.studentId.toString() === studentId); + + if (!student) { + throw createHttpError(404, "Student not in session"); + } + + const prevHoursAttended = student.hoursAttended; + let hoursLeft = enrollment.hoursLeft + prevHoursAttended; + + student.hoursAttended = hours; + hoursLeft -= student.hoursAttended; + enrollment.hoursLeft = hoursLeft > 0 ? hoursLeft : 0; + + await sessionObject.save(); + await enrollment.save(); + + res.status(200).send("Updated"); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/controllers/session.ts b/backend/src/controllers/session.ts index 74971609..dd58c0ae 100644 --- a/backend/src/controllers/session.ts +++ b/backend/src/controllers/session.ts @@ -266,6 +266,18 @@ export const updateSession: RequestHandler = async (req, res, next) => { return res.status(404).json({ message: "No object in database with provided ID" }); } + programData.students.forEach(async (student: StudentInfo) => { + const enrollment = await EnrollmentModel.findOne({ + studentId: student.studentId, + programId: programData.programId, + }); + if (enrollment) { + const hours = enrollment.hoursLeft - student.hoursAttended; + enrollment.hoursLeft = hours > 0 ? hours : 0; + await enrollment.save(); + } + }); + const absentStudents = programData.students.filter((student: StudentInfo) => !student.attended); const absenceSessions = absentStudents.map((absentStudent) => ({ diff --git a/backend/src/controllers/types/calendarTypes.ts b/backend/src/controllers/types/calendarTypes.ts index fdf2badf..6a9d4b32 100644 --- a/backend/src/controllers/types/calendarTypes.ts +++ b/backend/src/controllers/types/calendarTypes.ts @@ -1,9 +1,11 @@ +export type CalendarSlot = { + date: Date; + hours: number; + session: string; +}; + export type Calendar = { studentId: string; programId: string; - calendar: { - date: Date; - hours: number; - session: string; - }[]; + calendar: CalendarSlot[]; }; diff --git a/backend/src/routes/calendar.ts b/backend/src/routes/calendar.ts index a53ae2b0..d9aca799 100644 --- a/backend/src/routes/calendar.ts +++ b/backend/src/routes/calendar.ts @@ -5,11 +5,16 @@ import express from "express"; import * as CalendarController from "../controllers/calendar"; import { verifyAuthToken } from "../validators/auth"; +import * as CalendarValidator from "../validators/calendar"; const router = express.Router(); -router.get("/all", [verifyAuthToken]); router.get("/:studentId/:programId", [verifyAuthToken], CalendarController.getCalendar); -router.put("/:studentId/:programId", [verifyAuthToken]); +router.patch( + "/:studentId/:programId", + [verifyAuthToken], + CalendarValidator.editCalendar, + CalendarController.editCalendar, +); export default router; diff --git a/backend/src/validators/calendar.ts b/backend/src/validators/calendar.ts new file mode 100644 index 00000000..5877d9e8 --- /dev/null +++ b/backend/src/validators/calendar.ts @@ -0,0 +1,33 @@ +import { body } from "express-validator"; + +// const makeDateValidator = () => +// body("date") +// .exists() +// .withMessage("date needed") +// .bail() +// .isDate() +// .bail(); + +const makeHoursValidator = () => + body("hours") + .exists() + .withMessage("hours needed") + .bail() + .isNumeric() + .withMessage("needs to be number") + .bail(); + +const makeSessionValidator = () => + body("session") + .exists() + .withMessage("sessionId needed") + .bail() + .isString() + .withMessage("needs to be string") + .bail(); + +export const editCalendar = [ + // makeDateValidator(), + makeHoursValidator(), + makeSessionValidator(), +]; diff --git a/frontend/src/api/calendar.ts b/frontend/src/api/calendar.ts index 258cbcac..9664b25c 100644 --- a/frontend/src/api/calendar.ts +++ b/frontend/src/api/calendar.ts @@ -1,4 +1,4 @@ -import { GET, createAuthHeader, handleAPIError } from "../api/requests"; +import { GET, PATCH, createAuthHeader, handleAPIError } from "../api/requests"; import type { APIResult } from "../api/requests"; @@ -26,3 +26,21 @@ export async function getCalendar( return handleAPIError(error); } } + +export async function editCalendar( + studentId: string, + programId: string, + firebaseToken: string, + hours: number, + session: string, +): Promise> { + try { + const headers = createAuthHeader(firebaseToken); + const body = { hours, session }; + const res = await PATCH(`/calendar/${studentId}/${programId}`, body, headers); + const json = (await res.json()) as string; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx index f5d4ccb6..ec092b25 100644 --- a/frontend/src/components/Calendar/Calendar.tsx +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useState } from "react"; import Back from "../../../public/icons/back.svg"; -import { CalendarResponse, getCalendar } from "@/api/calendar"; +import { CalendarResponse, editCalendar, getCalendar } from "@/api/calendar"; import { Student, getStudent } from "@/api/students"; import { CalendarBody } from "@/components/Calendar/CalendarBody"; import LoadingSpinner from "@/components/LoadingSpinner"; @@ -25,6 +25,7 @@ export default function Calendar({ studentId, programId }: CalendarProps) { // const [currProgram, setProgram ] = useState(); const [calendar, setCalendar] = useState(); const [isLoading, setIsLoading] = useState(true); + const [firebaseToken, setFirebaseToken] = useState(""); const { windowSize } = useWindowSize(); const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); @@ -45,6 +46,7 @@ export default function Calendar({ studentId, programId }: CalendarProps) { setStudent(studentResponse.data); setIsLoading(false); } + setFirebaseToken(token); }) .catch((error) => { console.error(error); @@ -52,6 +54,10 @@ export default function Calendar({ studentId, programId }: CalendarProps) { } }, [firebaseUser]); + const updateCalendar = async (newHours: number, session: string) => { + await editCalendar(studentId, programId, firebaseToken, newHours, session); + }; + let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; let titleClass = "font-[alternate-gothic]"; @@ -91,10 +97,10 @@ export default function Calendar({ studentId, programId }: CalendarProps) {

{currStudent?.student.firstName + " " + currStudent?.student.lastName} - UCI # - 123456789 + {currStudent?.UCINumber}

- + )}
diff --git a/frontend/src/components/Calendar/CalendarBody.tsx b/frontend/src/components/Calendar/CalendarBody.tsx index 0e175976..c4a180f9 100644 --- a/frontend/src/components/Calendar/CalendarBody.tsx +++ b/frontend/src/components/Calendar/CalendarBody.tsx @@ -11,10 +11,14 @@ const poppins = Poppins({ weight: ["400", "700"], style: "normal", subsets: [] } export type CalendarBodyProps = { calendar?: CalendarResponse; + updateCalendarFunc: (newHours: number, session: string) => Promise; }; // -export const CalendarBody: React.FC = ({ calendar }: CalendarBodyProps) => { +export const CalendarBody: React.FC = ({ + calendar, + updateCalendarFunc, +}: CalendarBodyProps) => { const today = new Date(); const [month, changeMonth] = useState(today.getMonth()); @@ -86,7 +90,13 @@ export const CalendarBody: React.FC = ({ calendar }: Calendar
{dates.slice(0, dates.length).map((date, i) => ( - + ))}
diff --git a/frontend/src/components/Calendar/Datebox.tsx b/frontend/src/components/Calendar/Datebox.tsx index 847fd59d..9019ecd0 100644 --- a/frontend/src/components/Calendar/Datebox.tsx +++ b/frontend/src/components/Calendar/Datebox.tsx @@ -1,28 +1,39 @@ import React from "react"; export type DateboxProps = { + updateCalendarFunc: (newHours: number, session: string) => Promise; + session: string; day: number; hours: number; saturday?: boolean; }; -export function Datebox({ day, hours, saturday }: DateboxProps) { +export function Datebox({ updateCalendarFunc, session, day, hours, saturday }: DateboxProps) { let boxClass = "border-r border-t p-4 flex flex-col items-center"; if (saturday) { boxClass = "border-t p-4 flex flex-col items-center"; } + const updateCalendar = async (event: React.ChangeEvent) => { + const newHours = Number(event.target.value); + if (isNaN(newHours)) { + return; + } + await updateCalendarFunc(newHours, session); + }; + return (
{day}
- {hours !== 0 && ( + {hours !== -1 && ( )} - {hours === 0 && ( + {hours === -1 && ( )}
diff --git a/frontend/src/components/Calendar/types.ts b/frontend/src/components/Calendar/types.ts index 54b7dd57..69678cec 100644 --- a/frontend/src/components/Calendar/types.ts +++ b/frontend/src/components/Calendar/types.ts @@ -3,6 +3,7 @@ export type Day = { year: number; day: number; hours: number; + session: string; }; export const Months: string[] = [ diff --git a/frontend/src/components/Calendar/util.ts b/frontend/src/components/Calendar/util.ts index f40c32d0..c63537e4 100644 --- a/frontend/src/components/Calendar/util.ts +++ b/frontend/src/components/Calendar/util.ts @@ -20,7 +20,8 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe for (let i = 0; i < startDay; i++) { const date = new Date(year, month, i - startDay + 1); - let hours = 0; + let hours = -1; + let session = ""; if (calendar) { for (const c of calendar.calendar) { const calendarDate = new Date(c.date); @@ -30,16 +31,24 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe calendarDate.getFullYear() === date.getFullYear() ) { hours = c.hours; + session = c.session; } } } - days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); + days.push({ + month: date.getMonth(), + year: date.getFullYear(), + day: date.getDate(), + hours, + session, + }); } // get current month days for (let day = 1; day <= last.getDate(); day++) { const date = new Date(year, month, day); - let hours = 0; + let hours = -1; + let session = ""; if (calendar) { for (const c of calendar.calendar) { const calendarDate = new Date(c.date); @@ -49,10 +58,17 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe calendarDate.getFullYear() === date.getFullYear() ) { hours = c.hours; + session = c.session; } } } - days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); + days.push({ + month: date.getMonth(), + year: date.getFullYear(), + day: date.getDate(), + hours, + session, + }); } // get days after end of month @@ -60,7 +76,8 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe const endDate = last.getDate(); for (let i = endDay + 1; i <= 6; i++) { const date = new Date(year, month, i - endDay + endDate); - let hours = 0; + let hours = -1; + let session = ""; if (calendar) { for (const c of calendar.calendar) { const calendarDate = new Date(c.date); @@ -70,10 +87,17 @@ export const generateDates = (month: number, year: number, calendar?: CalendarRe calendarDate.getFullYear() === date.getFullYear() ) { hours = c.hours; + session = c.session; } } } - days.push({ month: date.getMonth(), year: date.getFullYear(), day: date.getDate(), hours }); + days.push({ + month: date.getMonth(), + year: date.getFullYear(), + day: date.getDate(), + hours, + session, + }); } return days; From 62f6a9da878cb50753b9674a26f285ee390b7963 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Thu, 9 Jan 2025 16:39:06 -0800 Subject: [PATCH 06/14] fixed issue with program enrollment --- backend/src/controllers/program.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/controllers/program.ts b/backend/src/controllers/program.ts index 37105ed7..e8fc1358 100644 --- a/backend/src/controllers/program.ts +++ b/backend/src/controllers/program.ts @@ -68,6 +68,12 @@ export const updateProgram: RequestHandler = async (req, res, next) => { { $set: { status: "Waitlisted", dateUpdated: Date.now() } }, ); + // update days of week when changed + await EnrollmentModel.updateMany( + { programId: { $eq: programId }, status: { $eq: "Joined" } }, + { $set: { schedule: programData.daysOfWeek } }, + ); + res.status(200).json(editedProgram); } catch (error) { next(error); From aad1ff170561f272c1f2a555ac55cadc6b363dcc Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Thu, 9 Jan 2025 16:54:26 -0800 Subject: [PATCH 07/14] remove unnecssary logs --- frontend/src/api/user.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 89418586..ed2d5333 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -30,7 +30,6 @@ export const verifyUser = async (firebaseToken: string): Promise export async function getNotApprovedUsers(firebaseToken: string): Promise> { try { const headers = createAuthHeader(firebaseToken); - console.log(headers); const response = await GET("/user/not-approved", headers); const json = (await response.json()) as User[]; return { success: true, data: json }; From 27d2b4dbc43ec34dd3f11744f5c08da7c67f3767 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Thu, 30 Jan 2025 11:57:00 -0800 Subject: [PATCH 08/14] fix student form requirements --- backend/src/controllers/student.ts | 5 +- backend/src/models/student.ts | 41 +++---- backend/src/validators/student.ts | 107 +++++++----------- .../components/StudentForm/StudentForm.tsx | 51 ++++++--- frontend/src/components/StudentProfile.tsx | 8 +- 5 files changed, 105 insertions(+), 107 deletions(-) diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index bfd38601..3d18a115 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -127,6 +127,9 @@ export const getAllStudents: RequestHandler = async (req, res, next) => { // Ensure that documents that are marked admin are not returned to non-admin users if (accountType !== "admin") { students.forEach((student) => { + if (!student.documents) { + return; + } student.documents = student.documents.filter( (doc) => !doc.markedAdmin, ) as typeof student.documents; @@ -155,7 +158,7 @@ export const getStudent: RequestHandler = async (req, res, next) => { } // Ensure that documents that are marked admin are not returned to non-admin users - if (accountType !== "admin") { + if (studentData.documents && accountType !== "admin") { studentData.documents = studentData.documents.filter( (doc) => !doc.markedAdmin, ) as typeof studentData.documents; diff --git a/backend/src/models/student.ts b/backend/src/models/student.ts index ac19c756..1628011e 100644 --- a/backend/src/models/student.ts +++ b/backend/src/models/student.ts @@ -7,22 +7,22 @@ const studentSchema = new Schema({ student: { lastName: { type: String, required: true }, firstName: { type: String, required: true }, - email: { type: String, required: true }, - phoneNumber: { type: String, required: true }, + email: { type: String, required: false, default: "" }, + phoneNumber: { type: String, required: false, default: "" }, }, emergency: { - lastName: { type: String, required: true }, - firstName: { type: String, required: true }, - email: { type: String, required: true }, - phoneNumber: { type: String, required: true }, + lastName: { type: String, required: false, default: "" }, + firstName: { type: String, required: false, default: "" }, + email: { type: String, required: false, default: "" }, + phoneNumber: { type: String, required: false, default: "" }, }, serviceCoordinator: { - lastName: { type: String, required: true }, - firstName: { type: String, required: true }, - email: { type: String, required: true }, - phoneNumber: { type: String, required: true }, + lastName: { type: String, required: false, default: "" }, + firstName: { type: String, required: false, default: "" }, + email: { type: String, required: false, default: "" }, + phoneNumber: { type: String, required: false, default: "" }, }, enrollments: { @@ -33,18 +33,18 @@ const studentSchema = new Schema({ }, //Address of student - location: { type: String, required: true }, + location: { type: String, required: false, default: "" }, //String list of medications - medication: { type: String, required: true }, + medication: { type: String, required: false, default: "" }, - birthday: { type: Date, required: true }, - intakeDate: { type: Date, required: true }, - tourDate: { type: Date, required: true }, + birthday: { type: Date, required: false, default: null }, + intakeDate: { type: Date, required: false, default: null }, + tourDate: { type: Date, required: false, default: null }, - conservation: { type: Boolean, required: true }, - UCINumber: { type: String, required: true }, - incidentForm: { type: String, required: true }, + conservation: { type: Boolean, required: false, default: false }, + UCINumber: { type: String, required: false, default: "" }, + incidentForm: { type: String, required: false, default: "" }, documents: { type: [ { @@ -53,7 +53,8 @@ const studentSchema = new Schema({ markedAdmin: { type: Boolean, required: true, default: false }, }, ], - required: true, + required: false, + default: [], }, profilePicture: { type: String, ref: "Image", required: false, default: "default" }, @@ -65,7 +66,7 @@ const studentSchema = new Schema({ }, //Will contain list of all dietary restrictions - dietary: { type: [String] }, + dietary: { type: [String], required: false, default: [] }, }); type Student = InferSchemaType; diff --git a/backend/src/validators/student.ts b/backend/src/validators/student.ts index 4c7b4499..d8f828f1 100644 --- a/backend/src/validators/student.ts +++ b/backend/src/validators/student.ts @@ -9,6 +9,10 @@ import { programValidatorUtil } from "../util/student"; //designed these to use the globstar operator from express-validator to more cleanly +const isValidDateTimeString = (value: string) => { + return !isNaN(Date.parse(value)); +}; + const makeIdValidator = () => body("**._id") .exists() @@ -63,76 +67,59 @@ const makePhoneNumbersValidator = () => //validate location const makeLocationValidator = () => - body("location") - .exists() - .withMessage("Location field fequired") - .bail() - .isString() - .withMessage("Location must be a string") - .bail() - .notEmpty() - .withMessage("Location field required"); + body("location").optional().isString().withMessage("Location must be a string").bail(); //medication const makeMedicationValidator = () => - body("medication") - .exists() - .withMessage("Medication field required") - .bail() - .isString() - .withMessage("Medication must be a string") - .bail() - .notEmpty() - .withMessage("Medication field required"); + body("medication").optional().isString().withMessage("Medication must be a string").bail(); //birthday const makeBirthdayValidator = () => body("birthday") - .exists() - .withMessage("Birthday field required") - .bail() - .isISO8601() - .toDate() - .withMessage("Birthday string must be a valid date-time string"); + .custom((value) => { + if (!value) { + return true; + } + if (!isValidDateTimeString(value as string)) { + throw new Error("Intake Date string must be a valid date-time string"); + } + return true; + }) + .toDate(); //intake date const makeIntakeDateValidator = () => body("intakeDate") - .exists() - .withMessage("Intake Date field required") - .bail() - .isISO8601() - .toDate() - .withMessage("Intake Date string must be a valid date-time string"); + .custom((value) => { + if (!value) { + return true; + } + if (!isValidDateTimeString(value as string)) { + throw new Error("Intake Date string must be a valid date-time string"); + } + return true; + }) + .toDate(); //tour date const makeTourDateValidator = () => body("tourDate") - .exists() - .withMessage("Tour Date field required") - .bail() - .isISO8601() - .toDate() - .withMessage("Tour Date string must be a valid date-time string"); + .custom((value) => { + if (!value) { + return true; + } + if (!isValidDateTimeString(value as string)) { + throw new Error("Intake Date string must be a valid date-time string"); + } + return true; + }) + .toDate(); const makeConservationValidator = () => - body("conservation") - .exists() - .withMessage("Conservation field required") - .bail() - .isBoolean() - .withMessage("Conservation must be a boolean"); + body("conservation").optional().isBoolean().withMessage("Conservation must be a boolean"); const makeUCINumberValidator = () => - body("UCINumber") - .exists() - .withMessage("UCI Number field required") - .bail() - .isString() - .withMessage("UCI Number must be a string") - .bail() - .notEmpty() - .withMessage("UCI Number field required"); + body("UCINumber").optional().isString().withMessage("UCI Number must be a string").bail(); type DocumentItem = { name: string; @@ -141,21 +128,11 @@ type DocumentItem = { }; const makeIncidentFormValidator = () => - body("incidentForm") - .exists() - .withMessage("Incident Form field required") - .bail() - .isString() - .withMessage("Incident Form must be a string") - .bail() - .notEmpty() - .withMessage("Incident Form field required"); + body("incidentForm").optional().isString().withMessage("Incident Form must be a string").bail(); const makeDocumentsValidator = () => body("documents") - .exists() - .withMessage("Documents field required") - .bail() + .optional() .isArray() .withMessage("Documents must be an array") .bail() @@ -187,9 +164,7 @@ const makeProfilePictureValidator = () => const makeEnrollments = () => body("enrollments") - .exists() - .withMessage("Enrollments field required") - .bail() + .optional() .isArray() .withMessage("Enrollments must be a non-empty array") .bail() diff --git a/frontend/src/components/StudentForm/StudentForm.tsx b/frontend/src/components/StudentForm/StudentForm.tsx index 9573a1fc..cba17728 100644 --- a/frontend/src/components/StudentForm/StudentForm.tsx +++ b/frontend/src/components/StudentForm/StudentForm.tsx @@ -66,6 +66,7 @@ export default function StudentForm({ const [currentFiles, setCurrentFiles] = useState([]); const [studentDocuments, setStudentDocuments] = useState(data?.documents ?? []); const [didDeleteOrMark, setDidDeleteOrMark] = useState(false); + const [success, setSuccess] = useState(false); const documentData = { currentFiles, @@ -110,6 +111,15 @@ export default function StudentForm({ } }, [firebaseUser]); + useEffect(() => { + if (success) { + setOpenSaveDialog(true); + setTimeout(() => { + setCurrentView("View"); + }, 1500); + } + }, [success]); + const onFormSubmit: SubmitHandler = async (formData: StudentFormData) => { const programAbbreviationToId = {} as Record; // abbreviation -> programId Object.values(allPrograms).forEach( @@ -214,9 +224,11 @@ export default function StudentForm({ setAllStudents((prevStudents: StudentMap | undefined) => { return { ...prevStudents, [newStudent._id]: { ...newStudent } }; }); + setSuccess(true); } else { console.log(result.error); alert("Unable to create student: " + result.error); + setSuccess(false); } }, (error) => { @@ -243,9 +255,11 @@ export default function StudentForm({ return prevStudents; } }); + setSuccess(true); } else { console.log(result.error); alert("Unable to edit student: " + result.error); + setSuccess(false); } }, (error) => { @@ -253,10 +267,6 @@ export default function StudentForm({ }, ); } - - setTimeout(() => { - setCurrentView("View"); - }, 1500); }; return ( @@ -305,21 +315,30 @@ export default function StudentForm({ setOpen={setOpenSaveDialog} onLeave={() => { router.push("/home"); + setCurrentView("View"); }} > {/* Save Dialog Content */} -
- - checkmark -

Student has been saved!

-
+ + {success && ( +
+ + checkmark +

Student has been saved!

+
+ )} ) : ( + + )}
Regular Programs:
From e73d8d88f10481af4ca68acb1cbdd74d8d0d7b8f Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Fri, 31 Jan 2025 09:39:47 -0800 Subject: [PATCH 10/14] fix medication field in student profile --- frontend/src/components/StudentForm/types.ts | 1 - frontend/src/components/StudentProfile.tsx | 13 +------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/frontend/src/components/StudentForm/types.ts b/frontend/src/components/StudentForm/types.ts index 775ae996..28bcae5c 100644 --- a/frontend/src/components/StudentForm/types.ts +++ b/frontend/src/components/StudentForm/types.ts @@ -34,7 +34,6 @@ export type StudentData = { documents: Document[]; profilePicture: string; enrollments: Enrollment[]; - dietary?: string[]; }; export type StudentFormData = { diff --git a/frontend/src/components/StudentProfile.tsx b/frontend/src/components/StudentProfile.tsx index 3bdf3e59..6e1025ff 100644 --- a/frontend/src/components/StudentProfile.tsx +++ b/frontend/src/components/StudentProfile.tsx @@ -484,18 +484,7 @@ export default function StudentProfile({ id }: StudentProfileProps) {
Medication & Medical
-
Dietary Restrictions:
-
- {studentData.dietary?.map((value) => ( -
- {value} -
-
- ))} -
-
- Medication: {studentData.medication} -
+
{studentData.medication}
{studentData.incidentForm && ( From 04cfc238194e406c337bc3a22218db1b072c40f2 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Fri, 31 Jan 2025 11:24:03 -0800 Subject: [PATCH 11/14] fix enrollments --- backend/src/models/enrollment.ts | 7 +- backend/src/util/student.ts | 2 +- .../StudentForm/EnrollmentsEdit.tsx | 107 +++++++++++++++--- frontend/src/components/StudentProfile.tsx | 44 ++++--- 4 files changed, 119 insertions(+), 41 deletions(-) diff --git a/backend/src/models/enrollment.ts b/backend/src/models/enrollment.ts index 0f8a0963..25fc888a 100644 --- a/backend/src/models/enrollment.ts +++ b/backend/src/models/enrollment.ts @@ -19,10 +19,11 @@ const enrollmentSchema = new mongoose.Schema({ schedule: { type: [String], required: true }, sessionTime: { type: { - start_time: { type: String, required: true }, - end_time: { type: String, required: true }, + start_time: { type: String, required: false, default: "00:00" }, + end_time: { type: String, required: false, default: "00:00" }, }, - required: true, + required: false, + default: { start_time: "00:00", end_time: "00:00" }, }, startDate: { type: Date, required: true }, renewalDate: { type: Date, required: true }, diff --git a/backend/src/util/student.ts b/backend/src/util/student.ts index c194c02b..7b4e74b0 100644 --- a/backend/src/util/student.ts +++ b/backend/src/util/student.ts @@ -10,11 +10,11 @@ export const programValidatorUtil = async (enrollments: Enrollment[]) => { "status", "hoursLeft", "schedule", - "sessionTime", "startDate", "renewalDate", "authNumber", ]; + enrollments.forEach((enrollment: Enrollment) => { requiredFields.forEach((field) => { if (!enrollment[field as keyof Enrollment]) diff --git a/frontend/src/components/StudentForm/EnrollmentsEdit.tsx b/frontend/src/components/StudentForm/EnrollmentsEdit.tsx index e3b5f319..2dbc0e06 100644 --- a/frontend/src/components/StudentForm/EnrollmentsEdit.tsx +++ b/frontend/src/components/StudentForm/EnrollmentsEdit.tsx @@ -30,7 +30,7 @@ export const emptyEnrollment = { programId: "", status: "", dateUpdated: new Date(), - hoursLeft: 8, + hoursLeft: 0, schedule: [] as string[], sessionTime: { start_time: "", @@ -134,9 +134,68 @@ const EnrollmentFormItem = ({ }, [item.programId]); // these 3 useEffects keep our custom dropdown and react-hook-form in sync + const calculateHoursLeft = ( + startDate: Date, + endDate: Date, + daysOfWeek: string[], + sessionStartTime: string, + sessionEndTime: string, + ): number => { + console.log(startDate, endDate, daysOfWeek, sessionStartTime, sessionEndTime); + const dayMap = { + Su: "Sunday", + M: "Monday", + T: "Tuesday", + W: "Wednesday", + Th: "Thursday", + F: "Friday", + Sa: "Saturday", + }; + + const createDateWithTime = (time: string): Date => { + const [hours, minutes] = time.split(":").map((t) => parseInt(t)); + const newDate = new Date(); + newDate.setHours(hours); + newDate.setMinutes(minutes); + return newDate; + }; + + const days = []; + for (const day of daysOfWeek) { + days.push(dayMap[day as keyof typeof dayMap]); + } + + const sessionStart = createDateWithTime(sessionStartTime); + const sessionEnd = createDateWithTime(sessionEndTime); + const sessionLength = (sessionEnd.getTime() - sessionStart.getTime()) / 1000 / 60 / 60; + + let totalHours = 0; + + for (let d = new Date(startDate); d <= new Date(endDate); d.setDate(d.getDate() + 1)) { + if (days.includes(d.toLocaleString("en-US", { weekday: "long" }))) { + totalHours += sessionLength; + } + } + console.log(totalHours); + + return totalHours; + }; + useEffect(() => { item.sessionTime = selectedSession; setValue(`${fieldName}.${index}.sessionTime`, selectedSession); + if (!varying && selectedSession.start_time && selectedSession.end_time) { + setValue( + `${fieldName}.${index}.hoursLeft`, + calculateHoursLeft( + new Date(item.startDate), + new Date(item.renewalDate), + item.schedule, + selectedSession.start_time, + selectedSession.end_time, + ), + ); + } }, [selectedSession]); useEffect(() => { @@ -198,19 +257,38 @@ const EnrollmentFormItem = ({ ); })} -
-

Session

- { - setSelectedSession(amPmToTime(value)); - }} - /> -
+ {!varying && ( +
+

Session

+ { + setSelectedSession(amPmToTime(value)); + }} + /> +
+ )} + + {varying && ( +
+

Hours Left

+ { + console.log(e.target.value); + setValue(`${fieldName}.${index}.hoursLeft`, Number(e.target.value)); + }} + /> +
+ )} {/*