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 82990b49..bfd38601 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"; @@ -9,6 +9,7 @@ import mongoose, { HydratedDocument } from "mongoose"; import EnrollmentModel from "../models/enrollment"; import { Image } from "../models/image"; +import ProgramModel from "../models/program"; import ProgressNoteModel from "../models/progressNote"; import StudentModel from "../models/student"; import { Enrollment } from "../types/enrollment"; @@ -81,6 +82,10 @@ export const editStudent: RequestHandler = async (req, res, next) => { enrollments.map(async (enrollment: Enrollment) => { const enrollmentExists = await EnrollmentModel.findById(enrollment._id); const enrollmentBody = { ...enrollment, studentId: new mongoose.Types.ObjectId(studentId) }; + const program = await ProgramModel.findById({ _id: enrollment.programId }); + if (program?.type === "regular") { + enrollmentBody.schedule = program.daysOfWeek; + } if (!enrollmentExists) { return await createEnrollment(enrollmentBody); } else { 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 cc7a8942..cc1a9c90 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 imageRouter from "./image"; import programRoutes from "./program"; import progressNoteRoutes from "./progressNote"; @@ -15,6 +16,7 @@ router.use("/student", studentRoutes); router.use("/program", programRoutes); router.use("/session", sessionRoutes); router.use("/progressNote", progressNoteRoutes); +router.use("/calendar", calendarRouter); router.use("/image", imageRouter); 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 4f187660..5baa0150 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/programs.ts b/frontend/src/api/programs.ts index 8180c877..97a908be 100644 --- a/frontend/src/api/programs.ts +++ b/frontend/src/api/programs.ts @@ -1,8 +1,6 @@ -import { GET, PATCH, POST, handleAPIError } from "../api/requests"; +import { GET, PATCH, POST, createAuthHeader, handleAPIError } from "../api/requests"; import { CreateProgramRequest } from "../components/ProgramForm/types"; -import { createAuthHeader } from "./progressNotes"; - import type { APIResult } from "../api/requests"; export type Program = CreateProgramRequest & { _id: string; dateUpdated: string }; 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/api/sessions.ts b/frontend/src/api/sessions.ts index 8ea97ade..d3f141a9 100644 --- a/frontend/src/api/sessions.ts +++ b/frontend/src/api/sessions.ts @@ -1,6 +1,4 @@ -import { GET, PATCH, POST, handleAPIError } from "../api/requests"; - -import { createAuthHeader } from "./progressNotes"; +import { GET, PATCH, POST, createAuthHeader, handleAPIError } from "../api/requests"; import type { APIResult } from "../api/requests"; diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index e4cce336..26ce0fae 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -1,8 +1,6 @@ -import { DELETE, GET, POST, PUT, handleAPIError } from "../api/requests"; +import { DELETE, GET, POST, PUT, createAuthHeader, handleAPIError } from "../api/requests"; import { StudentData as CreateStudentRequest } from "../components/StudentForm/types"; -import { createAuthHeader } from "./progressNotes"; - import type { APIResult } from "../api/requests"; export type Student = CreateStudentRequest & { diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx new file mode 100644 index 00000000..f5d4ccb6 --- /dev/null +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -0,0 +1,102 @@ +import Link from "next/link"; +import { useContext, useEffect, useMemo, useState } from "react"; + +import Back from "../../../public/icons/back.svg"; + +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 type CalendarProps = { + studentId: string; + programId: string; +}; + +export default function Calendar({ studentId, programId }: CalendarProps) { + useRedirectToLoginIfNotSignedIn(); + + const { firebaseUser } = useContext(UserContext); + + 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(() => { + 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, token); + if (studentResponse.success) { + setStudent(studentResponse.data); + setIsLoading(false); + } + }) + .catch((error) => { + console.error(error); + }); + } + }, [firebaseUser]); + + 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 +

+
+ +
+ )} +
+ ); +} 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/Datebox.tsx b/frontend/src/components/Calendar/Datebox.tsx new file mode 100644 index 00000000..847fd59d --- /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-4 flex flex-col items-center"; + if (saturday) { + boxClass = "border-t p-4 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..54b7dd57 --- /dev/null +++ b/frontend/src/components/Calendar/types.ts @@ -0,0 +1,23 @@ +export type Day = { + month: number; + year: number; + day: number; + hours: 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..83db774b --- /dev/null +++ b/frontend/src/components/Calendar/util.ts @@ -0,0 +1,65 @@ +import { Day } from "./types"; + +import { CalendarResponse } from "@/api/calendar"; + +/** + * 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, calendar?: CalendarResponse): 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); + 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); + 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 + 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); + 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/components/CalendarTable/CalendarTable.tsx b/frontend/src/components/CalendarTable/CalendarTable.tsx new file mode 100644 index 00000000..dcde0877 --- /dev/null +++ b/frontend/src/components/CalendarTable/CalendarTable.tsx @@ -0,0 +1,129 @@ +import { + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import React, { useContext, useEffect, useMemo, useState } from "react"; + +import LoadingSpinner from "../LoadingSpinner"; +import { Table } from "../ui/table"; + +// eslint-disable-next-line import/order +import Filter, { fuzzyFilter, programFilterFn, statusFilterFn } from "./Filters"; + +// 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 { StudentsContext } from "@/contexts/students"; +import { useWindowSize } from "@/hooks/useWindowSize"; +import { cn } from "@/lib/utils"; + +export default function CalendarTable() { + const { allStudents } = useContext(StudentsContext); + const [isLoading, setIsLoading] = useState(true); + const [calendarTable, setCalendarTable] = useState([]); + const [globalFilter, setGlobalFilter] = useState(""); + const { isTablet } = useWindowSize(); + + const { allPrograms } = useContext(ProgramsContext); + + useEffect(() => { + if (allStudents) { + setIsLoading(false); + } + }, [allStudents]); + + // Take all students and put them in rows for table + useEffect(() => { + if (!allStudents) { + return; + } + const tableRows: CalendarTableRow[] = Object.values(allStudents).flatMap((student) => { + // Generate a row for each program the student is enrolled in + return student.enrollments.map( + (enrollment) => + ({ + id: student._id, + profilePicture: "default", + student: `${student.student.firstName} ${student.student.lastName}`, + programs: { + programId: enrollment.programId, + status: enrollment.status, + dateUpdated: enrollment.dateUpdated, + hoursLeft: enrollment.hoursLeft, + studentId: student._id, + }, + }) 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..f1723885 --- /dev/null +++ b/frontend/src/components/CalendarTable/Filters.tsx @@ -0,0 +1,107 @@ +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 ( +
+ {/* 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.id === "programs" && header.column.getCanFilter()) { + return ( + + ); + } + return null; + })} + + ))} +
+ ); +} 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..aa03505f --- /dev/null +++ b/frontend/src/components/CalendarTable/types.ts @@ -0,0 +1,16 @@ +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: EnrollmentLink; +}; + +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..913fbdff --- /dev/null +++ b/frontend/src/components/CalendarTable/useColumnSchema.tsx @@ -0,0 +1,62 @@ +import Image from "next/image"; +import Link from "next/link"; +import { useMemo } from "react"; + +import { ProgramMap } from "../StudentsTable/types"; +import { ProgramPill } from "../StudentsTable/useColumnSchema"; + +import { Columns, EnrollmentLink } from "./types"; + +export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { + const columns: Columns = [ + { + accessorKey: "profilePicture", + header: "Profile Picture", + cell: (info) => ( +
+ Profile Picture +
+ ), + enableColumnFilter: false, + }, + { + accessorKey: "student", + header: "Student Name", + cell: (info) => ( + {info.getValue() as string} + ), + enableColumnFilter: false, + }, + { + accessorKey: "programs", + header: "Attendance", + cell: (info) => { + const enrollmentLink = info.getValue() as unknown as EnrollmentLink; + const studentId = enrollmentLink.studentId; + const programId = enrollmentLink.programId; + const program = allPrograms[programId]; + return ( + + + + ); + }, + }, + ]; + + return useMemo(() => columns, [allPrograms]); +} diff --git a/frontend/src/components/StudentsTable/useColumnSchema.tsx b/frontend/src/components/StudentsTable/useColumnSchema.tsx index 1d062f30..745175ce 100644 --- a/frontend/src/components/StudentsTable/useColumnSchema.tsx +++ b/frontend/src/components/StudentsTable/useColumnSchema.tsx @@ -15,7 +15,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..7178689b --- /dev/null +++ b/frontend/src/pages/calendar.tsx @@ -0,0 +1,16 @@ +import { useRouter } from "next/router"; + +import Calendar from "@/components/Calendar/Calendar"; +import CalendarTable from "@/components/CalendarTable/CalendarTable"; +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; + +export default function Component() { + useRedirectToLoginIfNotSignedIn(); + + const router = useRouter(); + const { student, program } = router.query; + + if (!student || !program) return ; + + return ; +}