diff --git a/backend/src/controllers/program.ts b/backend/src/controllers/program.ts index a2e93391..548ffed4 100644 --- a/backend/src/controllers/program.ts +++ b/backend/src/controllers/program.ts @@ -3,6 +3,7 @@ import { RequestHandler } from "express"; import { validationResult } from "express-validator"; //import { error } from "firebase-functions/logger"; +import EnrollmentModel from "../models/enrollment"; import ProgramModel from "../models/program"; import validationErrorParser from "../util/validationErrorParser"; @@ -15,6 +16,11 @@ export type Program = { color: string; //colorValueHex; hourlyPay: string; sessions: [string[]]; + archived: boolean; +}; + +export type ExistingProgram = Program & { + dateUpdated: string; }; export const createProgram: RequestHandler = async (req, res, next) => { @@ -23,7 +29,10 @@ export const createProgram: RequestHandler = async (req, res, next) => { try { validationErrorParser(errors); - const programForm = await ProgramModel.create(req.body as Program); + const programForm = await ProgramModel.create({ + ...(req.body as Program), + dateUpdated: new Date().toISOString(), + }); res.status(201).json(programForm); } catch (error) { @@ -37,22 +46,59 @@ export const updateProgram: RequestHandler = async (req, res, next) => { validationErrorParser(errors); const programId = req.params.id; - const programData = req.body as Program; + const programData = req.body as ExistingProgram; - const editedProgram = await ProgramModel.findOneAndUpdate({ _id: programId }, programData, { - new: true, - }); + const editedProgram = await ProgramModel.findOneAndUpdate( + { _id: programId }, + { ...programData, archived: false, dateUpdated: new Date().toISOString() }, //stand-in method of un-archiving programs + { + new: true, + }, + ); if (!editedProgram) { return res.status(404).json({ message: "No object in database with provided ID" }); } + // Waitlist all archived students. Making sure to only waitlist Archived students + // will prevent enrollments from being updated every time the program is updated + await EnrollmentModel.updateMany( + { programId: { $eq: programId }, status: { $eq: "Archived" } }, + { $set: { status: "Waitlisted", dateUpdated: Date.now() } }, + ); + res.status(200).json(editedProgram); } catch (error) { next(error); } }; +export const archiveProgram: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); + try { + validationErrorParser(errors); + + const programId = req.params.id; + const program = await ProgramModel.findByIdAndUpdate( + programId, + { $set: { archived: true, dateUpdated: new Date().toISOString() } }, + { new: true }, + ); + if (!program) + return res.status(404).json({ message: "Program with this id not found in database" }); + + //Archive all students + await EnrollmentModel.updateMany( + { programId: { $eq: programId } }, + { $set: { status: "Archived", dateUpdated: Date.now() } }, + ); + + return res.status(200).json(program); + } catch (error) { + next(error); + } +}; + export const getAllPrograms: RequestHandler = async (req, res, next) => { try { const programs = await ProgramModel.find(); diff --git a/backend/src/models/program.ts b/backend/src/models/program.ts index bc39d1d7..ac4fa3fd 100644 --- a/backend/src/models/program.ts +++ b/backend/src/models/program.ts @@ -8,6 +8,9 @@ const programSchema = new Schema({ color: { type: String, required: true }, hourlyPay: { type: Number, required: true }, sessions: { type: [[String]], required: true }, + archived: { type: Boolean, required: true }, + + dateUpdated: { type: String, required: true }, }); type Program = InferSchemaType; diff --git a/backend/src/routes/program.ts b/backend/src/routes/program.ts index 05e24645..df2af7df 100644 --- a/backend/src/routes/program.ts +++ b/backend/src/routes/program.ts @@ -11,6 +11,7 @@ const router = express.Router(); router.patch("/:id", ProgramValidator.updateProgram, ProgramController.updateProgram); router.post("/create", ProgramValidator.createProgram, ProgramController.createProgram); +router.post("/archive/:id", ProgramController.archiveProgram); router.get("/all", ProgramController.getAllPrograms); export default router; diff --git a/frontend/src/api/programs.ts b/frontend/src/api/programs.ts index 83b82f2f..9c339b0b 100644 --- a/frontend/src/api/programs.ts +++ b/frontend/src/api/programs.ts @@ -3,7 +3,7 @@ import { CreateProgramRequest } from "../components/ProgramForm/types"; import type { APIResult } from "../api/requests"; -export type Program = CreateProgramRequest & { _id: string }; +export type Program = CreateProgramRequest & { _id: string; dateUpdated: string }; export async function createProgram(program: CreateProgramRequest): Promise> { try { @@ -46,3 +46,13 @@ export async function getAllPrograms(): Promise> { return handleAPIError(error); } } + +export async function archiveProgram(program: Program): Promise> { + try { + const response = await POST(`/program/archive/${program._id}`, undefined); + const json = (await response.json()) as Program; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/components/AlertCard.tsx b/frontend/src/components/AlertCard.tsx new file mode 100644 index 00000000..e5224369 --- /dev/null +++ b/frontend/src/components/AlertCard.tsx @@ -0,0 +1,35 @@ +import { MouseEventHandler } from "react"; + +export default function AlertCard({ + message, + open, + onClose, +}: { + message: string; + open: boolean; + onClose: MouseEventHandler; +}) { + if (!open) return <>; + return ( +
+
+
+ + + +

{message}

+
+
+
+ ); +} diff --git a/frontend/src/components/ProgramCard.tsx b/frontend/src/components/ProgramCard.tsx index b54da3b8..54db111d 100644 --- a/frontend/src/components/ProgramCard.tsx +++ b/frontend/src/components/ProgramCard.tsx @@ -16,6 +16,8 @@ export type CardProps = { isAdmin: boolean; className?: string; setPrograms: React.Dispatch>; + setAlertState: React.Dispatch>; + archiveView?: boolean; }; // function checkOffscreen(id: string) { @@ -53,7 +55,14 @@ function toggleEdit(id: string) { // } } -export function ProgramCard({ program, isAdmin, className, setPrograms }: CardProps) { +export function ProgramCard({ + program, + isAdmin, + className, + setPrograms, + setAlertState, + archiveView = false, +}: CardProps) { const { isTablet } = useWindowSize(); const editId = "edit" + program._id; @@ -94,8 +103,12 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr hourlyPay: program.hourlyPay, sessions: program.sessions, //students: program.students, + archived: program.archived, + dateUpdated: program.dateUpdated, }; + const date = new Date(program.dateUpdated); + if (isTablet) { editClass += " top-7 w-12 h-5 text-[10px]"; outerDivClass += " rounded-lg h-36"; @@ -166,6 +179,7 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr component={editButton} data={programFields} setPrograms={setPrograms} + setAlertState={setAlertState} />
@@ -174,7 +188,7 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr

{program.type} Program

{program.name}

- {isAdmin && ( + {isAdmin && !archiveView && (
- students + {!archiveView && ( + students + )} {/*program.students.length === 0 &&

No Students

*/} {/*program.students.length === 1 &&

1 Student

*/} { //program.students.length > 1 && ( -

{/*program.students.length*/}0 Students

+

+ {/*program.students.length*/} + { + archiveView + ? "Archived on " + + (date.getMonth() + 1) + + "/" + + date.getDate() + + "/" + + date.getFullYear() + : "0 Students" //<---- Change in the future -------- + } +

//) }
diff --git a/frontend/src/components/ProgramForm/ProgramArchive.tsx b/frontend/src/components/ProgramForm/ProgramArchive.tsx index 480421f8..9fb9c18f 100644 --- a/frontend/src/components/ProgramForm/ProgramArchive.tsx +++ b/frontend/src/components/ProgramForm/ProgramArchive.tsx @@ -1,8 +1,9 @@ -import { useState } from "react"; +import { MouseEventHandler, useState } from "react"; import { useForm } from "react-hook-form"; -import { Program } from "../../api/programs"; +import { Program, archiveProgram } from "../../api/programs"; import { Button } from "../Button"; +import { ProgramMap } from "../StudentsTable/types"; import { Textfield } from "../Textfield"; import { Dialog, DialogClose, DialogContentSlide, DialogTrigger } from "../ui/dialog"; @@ -14,6 +15,8 @@ type archiveProps = { setOpenParent: React.Dispatch>; data: Program; isMobile?: boolean; + setPrograms: React.Dispatch>; + setAlertState: React.Dispatch>; }; function ProgramArchiveHeader({ label }: props) { @@ -48,7 +51,13 @@ function ProgramArchiveHeader({ label }: props) { } //Currently no functionality, just a button that closes the form -export default function ProgramArchive({ setOpenParent, data, isMobile = false }: archiveProps) { +export default function ProgramArchive({ + setOpenParent, + data, + isMobile = false, + setPrograms, + setAlertState, +}: archiveProps) { const [openArchive, setOpenArchive] = useState(false); const { register: archiveRegister, @@ -57,21 +66,55 @@ export default function ProgramArchive({ setOpenParent, data, isMobile = false } getValues: getArchiveValue, } = useForm<{ date: string }>(); + const archive: MouseEventHandler = () => { + const date = new Date(getArchiveValue("date")); + const today = new Date(); + if ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ) { + archiveProgram(data) + .then((result) => { + if (result.success) { + console.log(result.data); + archiveReset(); + setOpenArchive(false); + setOpenParent(false); + setAlertState({ open: true, message: result.data.name + " has been archived" }); + setPrograms((prevPrograms: ProgramMap) => { + if (Object.keys(prevPrograms).includes(result.data._id)) + return { ...prevPrograms, [result.data._id]: { ...result.data } }; + else console.log("Program ID does not exist"); + return prevPrograms; + }); + } else { + console.log(result.error); + alert("Unable to archive program: " + result.error); + } + }) + .catch((error) => { + console.log(error); + }); + } + }; + return ( -
+
{!isMobile ? (
{" "} diff --git a/frontend/src/components/ProgramForm/ProgramCancel.tsx b/frontend/src/components/ProgramForm/ProgramCancel.tsx index d3be0663..6103288e 100644 --- a/frontend/src/components/ProgramForm/ProgramCancel.tsx +++ b/frontend/src/components/ProgramForm/ProgramCancel.tsx @@ -22,7 +22,14 @@ export default function ProgramCancel({ isMobile = false, open, setOpen, onCance }} /> ) : ( -
Cancel
+
{ + setOpen(true); + }} + > + Cancel +
)} diff --git a/frontend/src/components/ProgramForm/types.ts b/frontend/src/components/ProgramForm/types.ts index 424a8d7c..114cff0b 100644 --- a/frontend/src/components/ProgramForm/types.ts +++ b/frontend/src/components/ProgramForm/types.ts @@ -6,6 +6,8 @@ export type ProgramData = { color: string; //colorValueHex; hourlyPay: string; sessions: [string[]]; + archived: boolean; + dateUpdated: string; }; export type CreateProgramRequest = { @@ -16,4 +18,5 @@ export type CreateProgramRequest = { color: string; hourlyPay: string; sessions: string[][]; + archived: boolean; }; diff --git a/frontend/src/components/ProgramFormButton.tsx b/frontend/src/components/ProgramFormButton.tsx index b4431a22..4da436ca 100644 --- a/frontend/src/components/ProgramFormButton.tsx +++ b/frontend/src/components/ProgramFormButton.tsx @@ -17,6 +17,7 @@ type BaseProperties = { classname?: string; component: React.JSX.Element; setPrograms: React.Dispatch>; + setAlertState: React.Dispatch>; }; type EditProperties = BaseProperties & { @@ -36,6 +37,7 @@ export default function ProgramFormButton({ component =

Please add a component

, data = null, setPrograms, + setAlertState, classname, }: ProgramFormProperties) { const { register, setValue: setCalendarValue, reset, handleSubmit } = useForm(); @@ -62,6 +64,7 @@ export default function ProgramFormButton({ color: formData.color, hourlyPay: formData.hourlyPay, sessions: programRequestType === "regular" ? sanitizedSessions : [], + archived: formData.archived ? formData.archived : false, }; console.log(`${type} program`, programRequest); @@ -71,6 +74,7 @@ export default function ProgramFormButton({ if (result.success) { setOpenForm(false); console.log(`${type} program`, result.data); + setAlertState({ open: true, message: "Added program " + result.data.name }); setPrograms((prevPrograms: ProgramMap) => { return { ...prevPrograms, [result.data._id]: { ...result.data } }; }); @@ -85,18 +89,22 @@ export default function ProgramFormButton({ }); } if (type === "edit" && data) { - const updatedProgram: Program = { ...programRequest, _id: data._id }; + const updatedProgram: Program = { + ...programRequest, + _id: data._id, + dateUpdated: data.dateUpdated, + }; console.log(`${type} program`, updatedProgram); editProgram(updatedProgram) .then((result) => { if (result.success) { setOpenForm(false); console.log(`${type} program`, result.data); + setAlertState({ open: true, message: "Edited program " + result.data.name }); setPrograms((prevPrograms: ProgramMap) => { if (Object.keys(prevPrograms).includes(result.data._id)) return { ...prevPrograms, [result.data._id]: { ...result.data } }; else console.log("Program ID does not exist"); - alert("Program ID does not exist"); return prevPrograms; }); } else { @@ -122,7 +130,14 @@ export default function ProgramFormButton({ }} >
- {type === "edit" && data && } + {type === "edit" && data && ( + + )}

{type === "add" ? "Add new program" : data?.name} @@ -163,12 +178,19 @@ export default function ProgramFormButton({ setOpen={setOpenCancel} onCancel={() => { setOpenForm(false); + setOpenCancel(false); reset(); }} /> {type === "edit" && data && ( - + )} {type === "add" ? ( diff --git a/frontend/src/components/StudentFormButton.tsx b/frontend/src/components/StudentFormButton.tsx index fc0879a0..870e9cea 100644 --- a/frontend/src/components/StudentFormButton.tsx +++ b/frontend/src/components/StudentFormButton.tsx @@ -116,6 +116,7 @@ export default function StudentFormButton({ setAllStudents((prevStudents: StudentMap) => { return { ...prevStudents, [newStudent._id]: { ...newStudent } }; }); + console.log(newStudent); } else { console.log(result.error); alert("Unable to create student: " + result.error); @@ -143,6 +144,7 @@ export default function StudentFormButton({ return prevStudents; } }); + console.log(editedStudent); } else { console.log(result.error); alert("Unable to edit student: " + result.error); diff --git a/frontend/src/pages/programs.tsx b/frontend/src/pages/programs.tsx index 53e24a2c..42359aa6 100644 --- a/frontend/src/pages/programs.tsx +++ b/frontend/src/pages/programs.tsx @@ -1,6 +1,7 @@ import Image from "next/image"; -import React, { useContext, useMemo } from "react"; +import React, { useContext, useMemo, useState } from "react"; +import AlertCard from "../components/AlertCard"; import { ProgramCard } from "../components/ProgramCard"; import ProgramFormButton from "../components/ProgramFormButton"; import { useWindowSize } from "../hooks/useWindowSize"; @@ -18,17 +19,27 @@ export default function Programs() { const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); + //const [programs, setPrograms] = useState({}); + const [archiveView, setArchiveView] = useState(false); + const [sliderOffset, setSliderOffset] = useState(0); + + const [alertState, setAlertState] = useState<{ open: boolean; message: string }>({ + open: false, + message: "", + }); + const { allPrograms: programs, setAllPrograms: setPrograms, isLoading, } = useContext(ProgramsContext); + const { isAdmin } = useContext(UserContext); let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; let titleClass = "font-[alternate-gothic]"; let headerClass = "flex flex-row"; - let cardsGridClass = "grid"; + let cardsGridClass = "grid ease-in animate-in fade-in-0"; let cardClass = ""; let emptyClass = "w-fill grow content-center justify-center"; @@ -80,16 +91,62 @@ export default function Programs() { ); + const selectorWrapper = + "relative sm:m-5 flex grid h-6 sm:min-h-12 w-full sm:w-[256px] grid-cols-2 divide-x border-none pointer-events-auto"; + const selectorClass = "flex h-full w-full items-center justify-center border-none"; + const radioClass = "peer flex h-full w-full appearance-none cursor-pointer"; + const selectorLabel = "absolute pointer-events-none sm:text-lg text-sm text-pia_dark_green"; + return (

Programs

{isAdmin && ( - + )} {/* Should be replaced with Add Button when created */}
+ +
+
+ { + setSliderOffset(0); + setArchiveView(false); + }} + /> +
Active
+
+
+ { + setSliderOffset(128); + setArchiveView(true); + }} + /> +
Archived
+
+
+
{isLoading ? ( ) : ( @@ -108,16 +165,32 @@ export default function Programs() {

)} {Object.keys(programs).length > 0 && ( -
- {Object.values(programs).map((program) => ( -
- -
- ))} +
+ {Object.values(programs).map((program) => + program.archived === archiveView || program.archived === undefined ? ( +
+ +
+ ) : ( + <> + ), + )}
)} )} + { + setAlertState({ ...alertState, open: false }); + }} + /> ); }