diff --git a/backend/src/controllers/program.ts b/backend/src/controllers/program.ts index f8f095c5..feca1913 100644 --- a/backend/src/controllers/program.ts +++ b/backend/src/controllers/program.ts @@ -16,7 +16,11 @@ export type Program = { color: string; //colorValueHex; hourlyPay: string; sessions: [string[]]; - archived?: boolean; + archived: boolean; +}; + +export type ExistingProgram = Program & { + dateUpdated: string; }; export const createProgram: RequestHandler = async (req, res, next) => { @@ -25,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) { @@ -39,11 +46,11 @@ 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, archived: false }, //stand-in method of un-archiving programs + { ...programData, archived: false, dateUpdated: new Date().toISOString() }, //stand-in method of un-archiving programs { new: true, }, @@ -55,12 +62,12 @@ export const updateProgram: RequestHandler = async (req, res, next) => { // Waitlist all archived students. Making sure to only waitlist Archived students // will prevent enrollments from being updated every time the program is updated - const updateReport = await EnrollmentModel.updateMany( + await EnrollmentModel.updateMany( { programId: { $eq: programId }, status: { $eq: "Archived" } }, { $set: { status: "Waitlisted", dateUpdated: Date.now() } }, ); - res.status(200).json({ ...editedProgram, updateReport }); + res.status(200).json(editedProgram); } catch (error) { next(error); } @@ -72,17 +79,22 @@ export const archiveProgram: RequestHandler = async (req, res, next) => { validationErrorParser(errors); const programId = req.params.id; - const program = await ProgramModel.findByIdAndUpdate(programId, { $set: { archived: true } }); + 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 - const updateReport = await EnrollmentModel.updateMany( + await EnrollmentModel.updateMany( { programId: { $eq: programId } }, { $set: { status: "Archived", dateUpdated: Date.now() } }, + { returnDocument: "after" }, ); - return res.status(200).json({ ...program, updateReport }); + return res.status(200).json(program); } catch (error) { next(error); } diff --git a/backend/src/models/program.ts b/backend/src/models/program.ts index ceb8b93a..ac4fa3fd 100644 --- a/backend/src/models/program.ts +++ b/backend/src/models/program.ts @@ -8,7 +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: false }, + archived: { type: Boolean, required: true }, + + dateUpdated: { type: String, required: true }, }); type Program = InferSchemaType; diff --git a/frontend/src/api/programs.ts b/frontend/src/api/programs.ts index b6484404..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 { diff --git a/frontend/src/components/AlertCard.tsx b/frontend/src/components/AlertCard.tsx new file mode 100644 index 00000000..6b81cf09 --- /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 911def7d..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; @@ -95,8 +104,11 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr 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"; @@ -167,6 +179,7 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr component={editButton} data={programFields} setPrograms={setPrograms} + setAlertState={setAlertState} />
@@ -175,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 fd078a92..1218be6c 100644 --- a/frontend/src/components/ProgramForm/ProgramArchive.tsx +++ b/frontend/src/components/ProgramForm/ProgramArchive.tsx @@ -3,6 +3,7 @@ import { useForm } from "react-hook-form"; 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, @@ -72,6 +81,13 @@ export default function ProgramArchive({ setOpenParent, data, isMobile = false } 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); diff --git a/frontend/src/components/ProgramForm/types.ts b/frontend/src/components/ProgramForm/types.ts index 45fb49fa..114cff0b 100644 --- a/frontend/src/components/ProgramForm/types.ts +++ b/frontend/src/components/ProgramForm/types.ts @@ -7,6 +7,7 @@ export type ProgramData = { hourlyPay: string; sessions: [string[]]; archived: boolean; + dateUpdated: string; }; export type CreateProgramRequest = { diff --git a/frontend/src/components/ProgramFormButton.tsx b/frontend/src/components/ProgramFormButton.tsx index 6ec0cece..e685aa64 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(); @@ -72,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 } }; }); @@ -86,13 +89,18 @@ 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 } }; @@ -122,7 +130,14 @@ export default function ProgramFormButton({ }} >
- {type === "edit" && data && } + {type === "edit" && data && ( + + )}

{type === "add" ? "Add new program" : data?.name} @@ -168,7 +183,13 @@ export default function ProgramFormButton({ /> {type === "edit" && data && ( - + )} {type === "add" ? ( diff --git a/frontend/src/pages/programs.tsx b/frontend/src/pages/programs.tsx index eda6f620..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, useState } from "react"; +import AlertCard from "../components/AlertCard"; import { ProgramCard } from "../components/ProgramCard"; import ProgramFormButton from "../components/ProgramFormButton"; import { useWindowSize } from "../hooks/useWindowSize"; @@ -22,6 +23,11 @@ export default function Programs() { 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, @@ -97,10 +103,16 @@ export default function Programs() {

Programs

{isAdmin && ( - + )} {/* Should be replaced with Add Button when created */}

+
program.archived === archiveView || program.archived === undefined ? (
- +
) : ( <> @@ -167,6 +185,12 @@ export default function Programs() { )} )} + { + setAlertState({ ...alertState, open: false }); + }} + /> ); }