diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index ef5e965..be3c77d 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -8,6 +8,8 @@ import { validationResult } from "express-validator"; import mongoose, { HydratedDocument } from "mongoose"; import EnrollmentModel from "../models/enrollment"; +import { Image } from "../models/image"; +import ProgressNoteModel from "../models/progressNote"; import StudentModel from "../models/student"; import { Enrollment } from "../types/enrollment"; import { createEnrollment, editEnrollment } from "../util/enrollment"; @@ -89,3 +91,46 @@ export const getAllStudents: RequestHandler = async (_, res, next) => { next(error); } }; + +export const getStudent: RequestHandler = async (req, res, next) => { + try { + const errors = validationResult(req); + + validationErrorParser(errors); + + const studentId = req.params.id; + const studentData = await StudentModel.findById(req.params.id); + + if (!studentData) { + return res.status(404).json({ message: "Student not found" }); + } + + const enrollments = await EnrollmentModel.find({ studentId }); + + res.status(200).json({ ...studentData.toObject(), programs: enrollments }); + } catch (error) { + next(error); + } +}; + +export const deleteStudent: RequestHandler = async (req, res, next) => { + try { + const errors = validationResult(req); + validationErrorParser(errors); + + const studentId = req.params.id; + const deletedStudent = await StudentModel.findById(studentId); + if (!deletedStudent) { + return res.status(404).json({ message: "Student not found" }); + } + + await EnrollmentModel.deleteMany({ studentId }); + await ProgressNoteModel.deleteMany({ studentId }); + await Image.deleteMany({ userId: studentId }); + await StudentModel.deleteOne({ _id: studentId }); + + res.status(200).json(deletedStudent); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts index a534d31..b0fa4c4 100644 --- a/backend/src/routes/student.ts +++ b/backend/src/routes/student.ts @@ -5,12 +5,14 @@ import express from "express"; import * as StudentController from "../controllers/student"; +import { verifyAuthToken } from "../validators/auth"; import * as StudentValidator from "../validators/student"; - 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", [verifyAuthToken], StudentController.getStudent); +router.delete("/:id", [verifyAuthToken], StudentController.deleteStudent); export default router; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fd7308..6b053be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "react-day-picker": "^8.10.0", "react-dom": "^18", "react-hook-form": "^7.49.3", + "react-to-print": "^3.0.2", "tailwind-merge": "^2.2.0", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", @@ -10130,6 +10131,15 @@ } } }, + "node_modules/react-to-print": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.2.tgz", + "integrity": "sha512-FS/Z4LLq0bgWaxd7obygFQ8yRFdKW74iE8fIVjFFsPJWIXmuL8CIO+4me1Hj44lrlxQ00gscSNb3BRM8olbwXg==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19" + } + }, "node_modules/read-cache": { "version": "1.0.0", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index 2db45b7..40caa68 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "react-day-picker": "^8.10.0", "react-dom": "^18", "react-hook-form": "^7.49.3", + "react-to-print": "^3.0.2", "tailwind-merge": "^2.2.0", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", diff --git a/frontend/public/back-arrow.svg b/frontend/public/back-arrow.svg new file mode 100644 index 0000000..eb477f0 --- /dev/null +++ b/frontend/public/back-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/email.svg b/frontend/public/email.svg new file mode 100644 index 0000000..1823113 --- /dev/null +++ b/frontend/public/email.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/exclamation.svg b/frontend/public/exclamation.svg new file mode 100644 index 0000000..a4ec890 --- /dev/null +++ b/frontend/public/exclamation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/pencil.svg b/frontend/public/pencil.svg new file mode 100644 index 0000000..0ee21ad --- /dev/null +++ b/frontend/public/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/phone.svg b/frontend/public/phone.svg new file mode 100644 index 0000000..26283cb --- /dev/null +++ b/frontend/public/phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/profile-icon.svg b/frontend/public/profile-icon.svg new file mode 100644 index 0000000..d295805 --- /dev/null +++ b/frontend/public/profile-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/api/programs.ts b/frontend/src/api/programs.ts index d69a320..80987c3 100644 --- a/frontend/src/api/programs.ts +++ b/frontend/src/api/programs.ts @@ -13,7 +13,11 @@ export type Enrollment = { dateUpdated: Date; hoursLeft: number; schedule: string[]; - sessionTime: string[]; + sessionTime: { + start_time: string; + end_time: string; + }; + required: true; startDate: Date; renewalDate: Date; authNumber: string; diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index 8a1583c..229911a 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -1,6 +1,8 @@ -import { GET, POST, PUT, handleAPIError } from "../api/requests"; +import { DELETE, GET, POST, PUT, 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 & { @@ -8,6 +10,9 @@ export type Student = CreateStudentRequest & { medication: string; otherString: string; progressNotes?: string[]; + UCINumber?: string; + conservation?: boolean; + profilePicture?: string; }; export async function createStudent(student: CreateStudentRequest): Promise> { @@ -40,3 +45,28 @@ export async function getAllStudents(): Promise> { return handleAPIError(error); } } + +export async function getStudent(id: string, firebaseToken: string): Promise> { + try { + const headers = createAuthHeader(firebaseToken); + const response = await GET(`/student/${id}`, headers); + const json = (await response.json()) as Student; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function deleteStudent( + id: string, + firebaseToken: string, +): Promise> { + try { + const headers = createAuthHeader(firebaseToken); + const response = await DELETE(`/student/${id}`, undefined, headers); + const json = (await response.json()) as Student; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/components/Modals/ModalConfirmation.tsx b/frontend/src/components/Modals/ModalConfirmation.tsx index 0561190..86eadc1 100644 --- a/frontend/src/components/Modals/ModalConfirmation.tsx +++ b/frontend/src/components/Modals/ModalConfirmation.tsx @@ -5,6 +5,8 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from "../ui/dialog" type ModalConfirmationProps = { ref?: React.RefObject; + // Optional dialog styling + className?: string; icon: React.ReactNode; triggerElement: React.ReactNode; onCancelClick?: (e: React.MouseEvent) => void; @@ -15,6 +17,8 @@ type ModalConfirmationProps = { confirmText: string; kind: "primary" | "destructive"; nestedDialog?: React.ReactNode; + //Used for inner react content (between title and buttons) + innerContent?: React.ReactNode; //Used for nested dialog to close parent dialog isParentOpen?: boolean; setIsParentOpen?: React.Dispatch>; @@ -23,6 +27,7 @@ type ModalConfirmationProps = { const ModalConfirmation = forwardRef( ( { + className, icon, triggerElement, onCancelClick = () => {}, @@ -33,6 +38,7 @@ const ModalConfirmation = forwardRef( confirmText, kind, nestedDialog, + innerContent, isParentOpen, setIsParentOpen, }, @@ -43,12 +49,17 @@ const ModalConfirmation = forwardRef( {triggerElement}
{icon}

{title}

{description ?

{description}

: null} + {innerContent}
+ +
+
+
+
Medication & Medical
+
Dietary Restrictions:
+
+ {studentData.dietary.map((value) => ( +
+ {value} +
+
+ ))} +
+
Medication: {studentData.medication}
+
+ +
+
+
Regular Programs:
+ {enrollmentInfo.map((value, index) => { + if (value.type === "regular") + return ; + return <>; + })} +
+
+
Varying Programs:
+ {enrollmentInfo.map((value, index) => { + if (value.type === "varying") + return ; + return <>; + })} +
+
+ {/*
+
Notifications
+
+ Review information of new account creations below to approve or deny them.{" "} +
+ +
*/} +
+ +
+
+
  • This cannot be undone!
  • +
  • + This will remove this student from all enrolled programs and delete all notes + and documents. +
  • +
    +
    +
    + Enter the student's last name to proceed + +
    + + } + kind="destructive" + triggerElement={ + + } + confirmText="Delete" + icon={
    } + isParentOpen={deleteDialogOpen} + setIsParentOpen={setDeleteDialogOpen} + onConfirmClick={deleteStudentHandler} + /> + +
    + +
    +
    + + ) + ); +} diff --git a/frontend/src/components/StudentProfilePrintComponent.tsx b/frontend/src/components/StudentProfilePrintComponent.tsx new file mode 100644 index 0000000..035bac4 --- /dev/null +++ b/frontend/src/components/StudentProfilePrintComponent.tsx @@ -0,0 +1,112 @@ +import { LegacyRef } from "react"; + +import { Student } from "../api/students"; + +import { EnrollmentDisplayInfo } from "./StudentProfile"; + +type PrintComponentProps = { + data: Student; + contentRef: LegacyRef | undefined; + enrollments: EnrollmentDisplayInfo[]; +}; + +type PrintContactProps = { + firstName: string; + lastName: string; + email: string; + phoneNumber: string; + header: string; +}; +type PrintProgramProps = { + enrollmentInfo: EnrollmentDisplayInfo; +}; + +function formatDate(d: Date) { + const date = new Date(d); + return date.getMonth() + 1 + "/" + date.getDate() + "/" + date.getFullYear(); +} + +function Contact({ firstName, lastName, email, phoneNumber, header }: PrintContactProps) { + return ( +
    +

    {header}

    +

    {firstName + " " + lastName}

    +

    {email}

    +

    {phoneNumber}

    +
    + ); +} + +function Program({ enrollmentInfo }: PrintProgramProps) { + const regular = enrollmentInfo.type === "regular"; + return ( +
    +
    {enrollmentInfo.name}
    +
    {enrollmentInfo.abbreviation}
    +
    Status: {enrollmentInfo.status}
    +
    Start Date: {formatDate(enrollmentInfo.startDate)}
    +
    End Date: {formatDate(enrollmentInfo.renewalDate)}
    +
    Authorization Code: {enrollmentInfo.authNumber}
    + {regular && ( +
    + Session Time: + {" " + + enrollmentInfo.sessionTime.start_time + + " - " + + enrollmentInfo.sessionTime.end_time} +
    + )} + {regular &&
    Days of the Week: {enrollmentInfo.schedule.join(", ")}
    } +
    + ); +} + +export default function StudentProfilePrintComponent({ + data, + contentRef, + enrollments, +}: PrintComponentProps) { + return ( +
    +
    + + + +
    +
    Student Background:
    +
    Address: {data.location}
    +
    Birthdate: {formatDate(data.birthday)}
    +
    Student Information:
    +
    Intake Date: {formatDate(data.intakeDate)}
    +
    Tour Date: {formatDate(data.tourDate)}
    +
    Medication & Medical
    +
    Dietary Restrictions:
    +
    + {data.dietary.map((value) => ( +
  • {value}
  • + ))} +
    +
    Medication: {data.medication}
    +
    +
    +
    Regular Programs
    + {enrollments.map((value, index) => { + if (value.type === "regular") { + return ; + } + return <>; + })} +
    +
    +
    Varying Programs
    + {enrollments.map((value, index) => { + if (value.type === "varying") { + return ; + } + return <>; + })} +
    +
    +
    + ); +} diff --git a/frontend/src/components/StudentsTable/useColumnSchema.tsx b/frontend/src/components/StudentsTable/useColumnSchema.tsx index ef6a4ad..aa914a8 100644 --- a/frontend/src/components/StudentsTable/useColumnSchema.tsx +++ b/frontend/src/components/StudentsTable/useColumnSchema.tsx @@ -1,9 +1,10 @@ import { Poppins } from "next/font/google"; +import Image from "next/image"; +import Link from "next/link"; import { useMemo } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "../../components/ui/popover"; import { Contact, ProgramLink } from "../StudentForm/types"; -import StudentFormButton from "../StudentFormButton"; import { Columns, ProgramMap, StudentMap } from "./types"; @@ -178,11 +179,15 @@ export function useColumnSchema({ cell: (info) => { return (
    - + + view student +
    ); }, diff --git a/frontend/src/pages/student/[id].tsx b/frontend/src/pages/student/[id].tsx new file mode 100644 index 0000000..b9e0906 --- /dev/null +++ b/frontend/src/pages/student/[id].tsx @@ -0,0 +1,23 @@ +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +import StudentProfile from "@/components/StudentProfile"; +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; + +export default function Student() { + useRedirectToLoginIfNotSignedIn(); + const router = useRouter(); + const [studentID, setStudentID] = useState(); + + useEffect(() => { + const id = router.query.id as string; + setStudentID(id); + }); + + if (studentID !== undefined) + return ( +
    + +
    + ); +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 2d590a9..91bb412 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -138,7 +138,13 @@ module.exports = { DEFAULT: "#F3F3F3", }, pia_primary_white: { - DEFAULT: "#FFF", + DEFAULT: "#FFFFFF", + }, + pia_secondary_green: { + DEFAULT: "#4FA197", + }, + pia_orange: { + DEFAULT: "#FF7A5E", }, secondary: { DEFAULT: "hsl(var(--secondary))",