Skip to content

Commit

Permalink
Feature/mraysu/program archive (#96)
Browse files Browse the repository at this point in the history
* Archive Program Route

* Change program status to archived for students

* Added Archived field to program schema

* Implemented Archive Button

* Prevent students from 'joining' archived programs

* Lint fix

* Integrated new enrollment schema

* Archived Programs Page

* Fix merge issues

* Updated Archive Page View

* Mobile UI Adjustments

---------

Co-authored-by: mraysu <[email protected]>
  • Loading branch information
mraysu and mraysu authored Jun 15, 2024
1 parent b0b800a commit 6e67ad3
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 55 deletions.
56 changes: 51 additions & 5 deletions backend/src/controllers/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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) => {
Expand All @@ -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) {
Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions backend/src/models/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof programSchema>;
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 11 additions & 1 deletion frontend/src/api/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<APIResult<Program>> {
try {
Expand Down Expand Up @@ -46,3 +46,13 @@ export async function getAllPrograms(): Promise<APIResult<[Program]>> {
return handleAPIError(error);
}
}

export async function archiveProgram(program: Program): Promise<APIResult<Program>> {
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);
}
}
35 changes: 35 additions & 0 deletions frontend/src/components/AlertCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="absolute bottom-0 left-0 flex min-h-12 w-full justify-center sm:min-h-16">
<div className="z-20 flex max-h-8 min-w-[10%] max-w-[90%] items-center rounded-sm bg-black sm:max-h-12 sm:max-w-[40%]">
<div className="flex max-w-full flex-row items-center overflow-hidden " onClick={onClose}>
<svg
width="16"
height="16"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="m-4"
>
<path
d="M1.70711 0.292894C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292894C-0.0976311 0.683418 -0.0976311 1.31658 0.292893 1.70711L3.58579 5L0.292893 8.29289C-0.0976311 8.68342 -0.0976311 9.31658 0.292893 9.70711C0.683417 10.0976 1.31658 10.0976 1.70711 9.70711L5 6.41421L8.29289 9.70711C8.68342 10.0976 9.31658 10.0976 9.70711 9.70711C10.0976 9.31658 10.0976 8.68342 9.70711 8.29289L6.41421 5L9.70711 1.70711C10.0976 1.31658 10.0976 0.683418 9.70711 0.292894C9.31658 -0.0976301 8.68342 -0.0976301 8.29289 0.292894L5 3.58579L1.70711 0.292894Z"
fill="White"
/>
</svg>
<p className="truncate pr-4 text-sm text-white sm:text-lg">{message}</p>
</div>
</div>
</div>
);
}
48 changes: 38 additions & 10 deletions frontend/src/components/ProgramCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type CardProps = {
isAdmin: boolean;
className?: string;
setPrograms: React.Dispatch<React.SetStateAction<ProgramMap>>;
setAlertState: React.Dispatch<React.SetStateAction<{ open: boolean; message: string }>>;
archiveView?: boolean;
};

// function checkOffscreen(id: string) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -166,6 +179,7 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr
component={editButton}
data={programFields}
setPrograms={setPrograms}
setAlertState={setAlertState}
/>
</div>
<div className={outerDivClass}>
Expand All @@ -174,7 +188,7 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr
<p className={typeClass}>{program.type} Program</p>
<p className={titleClass}>{program.name}</p>
</div>
{isAdmin && (
{isAdmin && !archiveView && (
<div className={optionsDiv}>
<Image
id={optionsId}
Expand All @@ -189,18 +203,32 @@ export function ProgramCard({ program, isAdmin, className, setPrograms }: CardPr
</div>
<div className={botDivClass}>
<div className={numClass}>
<Image
alt="students"
src="/programs/Students.png"
height={12}
width={18}
className={iconClass}
/>
{!archiveView && (
<Image
alt="students"
src="/programs/Students.png"
height={12}
width={18}
className={iconClass}
/>
)}
{/*program.students.length === 0 && <p className={numTextClass}>No Students</p>*/}
{/*program.students.length === 1 && <p className={numTextClass}>1 Student</p>*/}
{
//program.students.length > 1 && (
<p className={numTextClass}>{/*program.students.length*/}0 Students</p>
<p className={numTextClass}>
{/*program.students.length*/}
{
archiveView
? "Archived on " +
(date.getMonth() + 1) +
"/" +
date.getDate() +
"/" +
date.getFullYear()
: "0 Students" //<---- Change in the future --------
}
</p>
//)
}
</div>
Expand Down
Loading

0 comments on commit 6e67ad3

Please sign in to comment.