diff --git a/.ebextensions/03_ssh_keys.config b/.ebextensions/03_ssh_keys.config deleted file mode 100644 index b2ab3d871f..0000000000 --- a/.ebextensions/03_ssh_keys.config +++ /dev/null @@ -1,33 +0,0 @@ -files: - "/tmp/add_authorized_keys.sh": - mode: "000755" - owner: root - group: root - content: | - #!/bin/bash - AUTHORIZED_KEYS="/home/ec2-user/.ssh/authorized_keys" - mkdir -p /home/ec2-user/.ssh - touch "$AUTHORIZED_KEYS" - chown ec2-user:ec2-user /home/ec2-user/.ssh - chmod 700 /home/ec2-user/.ssh - - add_key() { - local key="$1" - if ! grep -qF "$key" "$AUTHORIZED_KEYS"; then - echo "$key" >> "$AUTHORIZED_KEYS" - fi - } - - # Chris Pyle - add_key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMqV5gwot3utGLPGpAPWr8znU1cjMn1RE7jN8htvaOMt aws-eb" - - # Sean Walker - add_key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICf1UhVM+65hiKahvdvEj20ohDu+bZVS+btVFJTtg0oP seanwalker@Seans-MacBook-Pro-2.local" - - chown ec2-user:ec2-user "$AUTHORIZED_KEYS" - chmod 600 "$AUTHORIZED_KEYS" - -commands: - add_authorized_keys: - command: "/tmp/add_authorized_keys.sh" - ignoreErrors: false diff --git a/Dockerfile b/Dockerfile index 651f732444..7c884979a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage - compile TypeScript -FROM node:25 AS builder +FROM node:20 AS builder WORKDIR /app COPY package.json tsconfig.build.json ./ @@ -11,11 +11,11 @@ RUN cd src/backend && npx prisma generate RUN yarn build:shared RUN yarn build:backend -FROM platformatic/node-caged:25-slim +FROM node:20-slim WORKDIR /app # Install OpenSSL for Prisma (slim image needs this) -RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* && npm install -g yarn +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* COPY package.json ./ diff --git a/devContainerization/Dockerfile.backend.dev b/devContainerization/Dockerfile.backend.dev index 8a9900879d..83f5a0277c 100644 --- a/devContainerization/Dockerfile.backend.dev +++ b/devContainerization/Dockerfile.backend.dev @@ -1,4 +1,4 @@ -FROM node:25 +FROM node:20 COPY package.json tsconfig.build.json ./ COPY ./src/backend/package.json ./src/backend/tsconfig.json src/backend/ diff --git a/devContainerization/Dockerfile.frontend.dev b/devContainerization/Dockerfile.frontend.dev index c7f12c4e58..c1e46d93f3 100644 --- a/devContainerization/Dockerfile.frontend.dev +++ b/devContainerization/Dockerfile.frontend.dev @@ -1,4 +1,4 @@ -FROM node:25-alpine +FROM node:20-alpine COPY package.json tsconfig.build.json ./ COPY ./src/frontend/package.json ./src/frontend/tsconfig.json src/frontend/ @@ -11,4 +11,4 @@ COPY ./src/frontend src/frontend COPY ./src/shared src/shared EXPOSE 3000 -CMD [ "yarn", "workspace", "frontend", "vite", "--force", "--host" ] +CMD [ "yarn", "workspace", "frontend", "vite", "--force", "--host" ] \ No newline at end of file diff --git a/infrastructure/modules/elasticbeanstalk/variables.tf b/infrastructure/modules/elasticbeanstalk/variables.tf index 926cca7e9d..5d8e41e9d1 100644 --- a/infrastructure/modules/elasticbeanstalk/variables.tf +++ b/infrastructure/modules/elasticbeanstalk/variables.tf @@ -14,7 +14,7 @@ variable "solution_stack_name" { description = "Elastic Beanstalk solution stack name" type = string # Find the latest: aws elasticbeanstalk list-available-solution-stacks - default = "64bit Amazon Linux 2023 v4.11.0 running Docker" + default = "64bit Amazon Linux 2023 v4.7.4 running Docker" } variable "vpc_id" { diff --git a/infrastructure/modules/monitoring/main.tf b/infrastructure/modules/monitoring/main.tf index dcb010d15f..e8a145883a 100644 --- a/infrastructure/modules/monitoring/main.tf +++ b/infrastructure/modules/monitoring/main.tf @@ -357,10 +357,6 @@ resource "aws_cloudwatch_metric_alarm" "eb_memory_high" { Environment = var.environment Project = var.project_name } - - lifecycle { - ignore_changes = [metric_query] - } } ############# diff --git a/infrastructure/modules/network/main.tf b/infrastructure/modules/network/main.tf index 20bd21d86d..c11cd989bc 100644 --- a/infrastructure/modules/network/main.tf +++ b/infrastructure/modules/network/main.tf @@ -164,14 +164,6 @@ resource "aws_security_group" "eb_instance" { security_groups = [aws_security_group.alb.id] } - ingress { - description = "SSH access" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - egress { description = "Allow all outbound traffic" from_port = 0 diff --git a/infrastructure/scripts/ssh-to-eb.sh b/infrastructure/scripts/ssh-to-eb.sh index f285c9a5a8..54d26dd79b 100755 --- a/infrastructure/scripts/ssh-to-eb.sh +++ b/infrastructure/scripts/ssh-to-eb.sh @@ -58,7 +58,6 @@ echo "🔌 Connecting to EB instance..." echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Instance: $INSTANCE_IP ($INSTANCE_ID)" -echo "Direct SSH: ssh -i $KEY_PATH_EXPANDED ec2-user@$INSTANCE_IP" echo "User: ec2-user" echo "" echo "Useful commands once connected:" diff --git a/package.json b/package.json index ad2edbe643..36f1d40b15 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@babel/preset-typescript": "^7.18.6", "@types/canvas-confetti": "^1.9.0", "@types/jest": "^29.5.14", - "@types/node": "^25.0.0", + "@types/node": "20.0.0", "@typescript-eslint/eslint-plugin": "8.20.0", "@typescript-eslint/parser": "8.20.0", "concurrently": "^9.1.0", diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index 8c2714a61a..f2622b80bb 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -1,6 +1,5 @@ # TO BE RUN FROM DOCKER COMPOSE. DO NOT RUN MANUALLY AS CONTEXT IS NOT SET CORRECTLY -FROM platformatic/node-caged:25-slim -RUN npm install -g yarn +FROM node:20 WORKDIR /base diff --git a/src/backend/custom.d.ts b/src/backend/custom.d.ts index 39cdc3623c..964ba270d6 100644 --- a/src/backend/custom.d.ts +++ b/src/backend/custom.d.ts @@ -1,4 +1,4 @@ -import { Organization, Prisma } from '@prisma/client'; +import { Organization } from '@prisma/client'; import { User as SharedUser } from 'shared'; declare global { @@ -6,7 +6,6 @@ declare global { export interface Request { currentUser: SharedUser; organization: Organization; - currentCar?: Prisma.CarGetPayload<{ include: { wbsElement: true } }>; } } } diff --git a/src/backend/index.ts b/src/backend/index.ts index 6443801418..622aa31121 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -3,7 +3,6 @@ import cors from 'cors'; import cookieParser from 'cookie-parser'; import { getUserAndOrganization, prodHeaders, requireJwtDev, requireJwtProd } from './src/utils/auth.utils.js'; import { errorHandler } from './src/utils/errors.utils.js'; -import { getCurrentCar } from './src/utils/car.utils.js'; import userRouter from './src/routes/users.routes.js'; import projectRouter from './src/routes/projects.routes.js'; import teamsRouter from './src/routes/teams.routes.js'; @@ -28,7 +27,6 @@ import partsRouter from './src/routes/parts.routes.js'; import financeRouter from './src/routes/finance.routes.js'; import calendarRouter from './src/routes/calendar.routes.js'; import prospectiveSponsorRouter from './src/routes/prospective-sponsor.routes.js'; -import attendanceRouter from './src/routes/attendance.routes.js'; const app = express(); @@ -91,9 +89,6 @@ app.use(isProd ? requireJwtProd : requireJwtDev); // get user and organization app.use(getUserAndOrganization); -// get current car -app.use(getCurrentCar); - // routes app.use('/users', userRouter); app.use('/projects', projectRouter); @@ -117,7 +112,6 @@ app.use('/parts', partsRouter); app.use('/finance', financeRouter); app.use('/calendar', calendarRouter); app.use('/prospective-sponsors', prospectiveSponsorRouter); -app.use('/attendance', attendanceRouter); app.use('/', (_req, res) => { res.status(200).json('Welcome to FinishLine'); }); diff --git a/src/backend/package.json b/src/backend/package.json index 1204e16d7e..db4bc2e8d8 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -39,7 +39,7 @@ "devDependencies": { "@types/express-jwt": "^6.0.4", "@types/jsonwebtoken": "^8.5.9", - "@types/node": "^25.0.0", + "@types/node": "^20.0.0", "@types/supertest": "^2.0.12", "nodemon": "^2.0.16", "supertest": "^6.2.4", diff --git a/src/backend/src/controllers/attendance.controllers.ts b/src/backend/src/controllers/attendance.controllers.ts deleted file mode 100644 index 6430449f62..0000000000 --- a/src/backend/src/controllers/attendance.controllers.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NextFunction, Request, Response } from 'express'; -import AttendanceService from '../services/attendance.services.js'; - -export default class AttendanceController { - static async takeAttendance(req: Request, res: Response, next: NextFunction) { - try { - const { teamId, message } = req.body; - const attendance = await AttendanceService.takeAttendance(req.currentUser, teamId, message, req.organization); - res.status(200).json(attendance); - } catch (error: unknown) { - next(error); - } - } - - static async getAllAttendances(req: Request, res: Response, next: NextFunction) { - try { - const attendances = await AttendanceService.getAllAttendances(req.organization); - res.status(200).json(attendances); - } catch (error: unknown) { - next(error); - } - } - - static async getAttendanceById(req: Request, res: Response, next: NextFunction) { - try { - const { meetingAttendanceId } = req.params as Record; - const attendance = await AttendanceService.getAttendanceById(meetingAttendanceId, req.organization); - res.status(200).json(attendance); - } catch (error: unknown) { - next(error); - } - } - - static async getOngoingAttendance(req: Request, res: Response, next: NextFunction) { - try { - const { teamId } = req.params as Record; - const attendance = await AttendanceService.getOngoingAttendance(teamId, req.organization); - res.status(200).json(attendance); - } catch (error: unknown) { - next(error); - } - } - - static async closeOngoingAttendance(req: Request, res: Response, next: NextFunction) { - try { - const { teamId } = req.params as Record; - await AttendanceService.closeOngoingAttendance(teamId, req.currentUser, req.organization); - res.status(200).json({ message: 'Attendance closed successfully' }); - } catch (error: unknown) { - next(error); - } - } - - static async checkChannel(req: Request, res: Response, next: NextFunction) { - try { - const { teamId } = req.params as Record; - const result = await AttendanceService.checkTeamChannel(teamId, req.organization); - res.status(200).json(result); - } catch (error: unknown) { - next(error); - } - } -} diff --git a/src/backend/src/controllers/calendar.controllers.ts b/src/backend/src/controllers/calendar.controllers.ts index 6e1edac0a1..53377b9650 100644 --- a/src/backend/src/controllers/calendar.controllers.ts +++ b/src/backend/src/controllers/calendar.controllers.ts @@ -596,18 +596,4 @@ export default class CalendarController { next(error); } } - - static async getAllEventsPaginated(req: Request, res: Response, next: NextFunction) { - try { - const { cursor, pageSize } = req.body; - const paginatedEvents = await CalendarService.getAllEventsPaginated( - req.organization, - cursor ? new Date(cursor) : undefined, - pageSize ? parseInt(pageSize) : undefined - ); - res.status(200).json(paginatedEvents); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/controllers/change-requests.controllers.ts b/src/backend/src/controllers/change-requests.controllers.ts index 0dd94267ba..208937bac8 100644 --- a/src/backend/src/controllers/change-requests.controllers.ts +++ b/src/backend/src/controllers/change-requests.controllers.ts @@ -16,16 +16,7 @@ export default class ChangeRequestsController { static async getAllChangeRequests(req: Request, res: Response, next: NextFunction) { try { - const changeRequests = await ChangeRequestsService.getAllChangeRequests(req.organization, req.currentCar?.carId); - res.status(200).json(changeRequests); - } catch (error: unknown) { - next(error); - } - } - - static async getAllGuestChangeRequests(req: Request, res: Response, next: NextFunction) { - try { - const changeRequests = await ChangeRequestsService.getAllGuestChangeRequests(req.organization); + const changeRequests = await ChangeRequestsService.getAllChangeRequests(req.organization); res.status(200).json(changeRequests); } catch (error: unknown) { next(error); @@ -34,11 +25,7 @@ export default class ChangeRequestsController { static async getToReviewChangeRequests(req: Request, res: Response, next: NextFunction) { try { - const changeRequests = await ChangeRequestsService.getToReviewChangeRequests( - req.currentUser, - req.organization, - req.currentCar?.carId - ); + const changeRequests = await ChangeRequestsService.getToReviewChangeRequests(req.currentUser, req.organization); res.status(200).json(changeRequests); } catch (error: unknown) { next(error); @@ -54,8 +41,7 @@ export default class ChangeRequestsController { const changeRequests = await ChangeRequestsService.getUnreviewedChangeRequests( req.currentUser, validatedWbs, - req.organization, - req.currentCar?.carId + req.organization ); res.status(200).json(changeRequests); } catch (error: unknown) { @@ -72,8 +58,7 @@ export default class ChangeRequestsController { const changeRequests = await ChangeRequestsService.getApprovedChangeRequests( req.currentUser, validatedWbs, - req.organization, - req.currentCar?.carId + req.organization ); res.status(200).json(changeRequests); } catch (error: unknown) { diff --git a/src/backend/src/controllers/finance.controllers.ts b/src/backend/src/controllers/finance.controllers.ts index 016a0ce498..7caaefee22 100644 --- a/src/backend/src/controllers/finance.controllers.ts +++ b/src/backend/src/controllers/finance.controllers.ts @@ -172,17 +172,17 @@ export default class FinanceController { static async getReimbursementRequestTeamData(req: Request, res: Response, next: NextFunction) { try { const { teamId } = req.params as Record; - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const rrData = await FinanceServices.getReimbursementRequestTeamData( req.organization, teamId, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(rrData); } catch (error: unknown) { @@ -193,17 +193,17 @@ export default class FinanceController { static async getReimbursementRequestTeamTypeData(req: Request, res: Response, next: NextFunction) { try { const { teamTypeId } = req.params as Record; - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const rrData = await FinanceServices.getReimbursementRequestTeamTypeData( req.organization, teamTypeId, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(rrData); } catch (error: unknown) { @@ -214,17 +214,17 @@ export default class FinanceController { static async getSpendingBarTeamData(req: Request, res: Response, next: NextFunction) { try { const { teamId } = req.params as Record; - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const spendingBarData = await FinanceServices.getSpendingBarTeamData( req.organization, teamId, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(spendingBarData); } catch (error: unknown) { @@ -235,17 +235,17 @@ export default class FinanceController { static async getSpendingBarTeamTypeData(req: Request, res: Response, next: NextFunction) { try { const { teamTypeId } = req.params as Record; - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const spendingBarData = await FinanceServices.getSpendingBarTeamTypeData( req.organization, teamTypeId, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(spendingBarData); } catch (error: unknown) { @@ -255,16 +255,16 @@ export default class FinanceController { static async getAllReimbursementRequestData(req: Request, res: Response, next: NextFunction) { try { - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const rrData = await FinanceServices.getAllReimbursementRequestData( req.organization, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(rrData); } catch (error: unknown) { @@ -275,17 +275,17 @@ export default class FinanceController { static async getReimbursementRequestCategoryData(req: Request, res: Response, next: NextFunction) { try { const { otherReasonId } = req.params as Record; - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const rrData = await FinanceServices.getReimbursementRequestCategoryData( otherReasonId, req.organization, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(rrData); } catch (error: unknown) { @@ -295,16 +295,16 @@ export default class FinanceController { static async getAllSpendingBarData(req: Request, res: Response, next: NextFunction) { try { - const { startDate, endDate } = req.query; + const { startDate, endDate, carNumber } = req.query; const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; + const parsedCarNumber = typeof carNumber === 'string' ? Number(carNumber) : undefined; const spendingBarData = await FinanceServices.getAllSpendingBarData( req.organization, parsedStartDate, parsedEndDate, - carNumber + parsedCarNumber ); res.status(200).json(spendingBarData); } catch (error: unknown) { @@ -314,17 +314,7 @@ export default class FinanceController { static async getSpendingBarCategoryData(req: Request, res: Response, next: NextFunction) { try { - const { startDate, endDate } = req.query; - const parsedStartDate = typeof startDate === 'string' ? new Date(startDate) : undefined; - const parsedEndDate = typeof endDate === 'string' ? new Date(endDate) : undefined; - const carNumber = req.currentCar?.wbsElement.carNumber; - - const spendingBarData = await FinanceServices.getSpendingBarCategoryData( - req.organization, - parsedStartDate, - parsedEndDate, - carNumber - ); + const spendingBarData = await FinanceServices.getSpendingBarCategoryData(req.organization); res.status(200).json(spendingBarData); } catch (error: unknown) { next(error); diff --git a/src/backend/src/controllers/projects.controllers.ts b/src/backend/src/controllers/projects.controllers.ts index 058e6af3c8..a76df6cdd3 100644 --- a/src/backend/src/controllers/projects.controllers.ts +++ b/src/backend/src/controllers/projects.controllers.ts @@ -16,7 +16,7 @@ import BillOfMaterialsService from '../services/boms.services.js'; export default class ProjectsController { static async getAllProjectsGantt(req: Request, res: Response, next: NextFunction) { try { - const projects: ProjectGantt[] = await ProjectsService.getAllProjectsGantt(req.organization, req.currentCar?.carId); + const projects: ProjectGantt[] = await ProjectsService.getAllProjectsGantt(req.organization); res.status(200).json(projects); } catch (error: unknown) { next(error); @@ -25,7 +25,7 @@ export default class ProjectsController { static async getAllProjects(req: Request, res: Response, next: NextFunction) { try { - const projects: ProjectPreview[] = await ProjectsService.getAllProjects(req.organization, req.currentCar?.carId); + const projects: ProjectPreview[] = await ProjectsService.getAllProjects(req.organization); res.status(200).json(projects); } catch (error: unknown) { next(error); @@ -34,11 +34,7 @@ export default class ProjectsController { static async getUsersTeamsProjects(req: Request, res: Response, next: NextFunction) { try { - const projects: ProjectOverview[] = await ProjectsService.getUsersTeamsProjects( - req.currentUser, - req.organization, - req.currentCar?.carId - ); + const projects: ProjectOverview[] = await ProjectsService.getUsersTeamsProjects(req.currentUser, req.organization); res.status(200).json(projects); } catch (error: unknown) { next(error); @@ -47,11 +43,7 @@ export default class ProjectsController { static async getUsersLeadingProjects(req: Request, res: Response, next: NextFunction) { try { - const projects: ProjectOverview[] = await ProjectsService.getUsersLeadingProjects( - req.currentUser, - req.organization, - req.currentCar?.carId - ); + const projects: ProjectOverview[] = await ProjectsService.getUsersLeadingProjects(req.currentUser, req.organization); res.status(200).json(projects); } catch (error: unknown) { next(error); @@ -61,7 +53,7 @@ export default class ProjectsController { static async getTeamsProjects(req: Request, res: Response, next: NextFunction) { try { const { teamId } = req.params as Record; - const projects: Project[] = await ProjectsService.getTeamsProjects(req.organization, teamId, req.currentCar?.carId); + const projects: Project[] = await ProjectsService.getTeamsProjects(req.organization, teamId); res.status(200).json(projects); } catch (error: unknown) { next(error); diff --git a/src/backend/src/controllers/recruitment.controllers.ts b/src/backend/src/controllers/recruitment.controllers.ts index e4383427fd..9a3154d47d 100644 --- a/src/backend/src/controllers/recruitment.controllers.ts +++ b/src/backend/src/controllers/recruitment.controllers.ts @@ -115,23 +115,4 @@ export default class RecruitmentController { next(error); } } - - static async deleteGuestDefinition(req: Request, res: Response, next: NextFunction) { - try { - const { definitionId } = req.params as Record; - await RecruitmentServices.deleteGuestDefinition(req.currentUser, definitionId, req.organization); - res.status(200).json({ message: `Successfully deleted guestDefinition with id ${definitionId}` }); - } catch (error: unknown) { - next(error); - } - } - - static async getAllGuestDefintions(req: Request, res: Response, next: NextFunction) { - try { - const allDefinitons = await RecruitmentServices.getAllGuestDefinitions(req.organization); - res.status(200).json(allDefinitons); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/controllers/reimbursement-requests.controllers.ts b/src/backend/src/controllers/reimbursement-requests.controllers.ts index 5b29bb17cd..ed28d1b410 100644 --- a/src/backend/src/controllers/reimbursement-requests.controllers.ts +++ b/src/backend/src/controllers/reimbursement-requests.controllers.ts @@ -13,11 +13,9 @@ import { HttpException } from '../utils/errors.utils.js'; export default class ReimbursementRequestsController { static async getCurrentUserReimbursementRequests(req: Request, res: Response, next: NextFunction) { try { - const carNumber = req.currentCar?.wbsElement.carNumber; const userReimbursementRequests = await ReimbursementRequestService.getUserReimbursementRequests( req.currentUser, - req.organization, - carNumber + req.organization ); res.status(200).json(userReimbursementRequests); } catch (error: unknown) { @@ -27,11 +25,9 @@ export default class ReimbursementRequestsController { static async getCurrentUserAssignedReimbursementRequests(req: Request, res: Response, next: NextFunction) { try { - const carNumber = req.currentCar?.wbsElement.carNumber; const assignedReimbursementRequests = await ReimbursementRequestService.getUserAssignedReimbursementRequests( req.currentUser, - req.organization, - carNumber + req.organization ); res.status(200).json(assignedReimbursementRequests); } catch (error: unknown) { @@ -50,11 +46,9 @@ export default class ReimbursementRequestsController { static async getCurrentUsersTeamsReimbursementRequests(req: Request, res: Response, next: NextFunction) { try { - const carNumber = req.currentCar?.wbsElement.carNumber; const userTeamsReimbursementRequests = await ReimbursementRequestService.getUsersTeamsReimbursementRequests( req.currentUser, - req.organization, - carNumber + req.organization ); res.status(200).json(userTeamsReimbursementRequests); } catch (error: unknown) { @@ -214,11 +208,9 @@ export default class ReimbursementRequestsController { static async getPendingAdvisorList(req: Request, res: Response, next: NextFunction) { try { - const carNumber = req.currentCar?.wbsElement.carNumber; const requestsPendingAdvisors: ReimbursementRequest[] = await ReimbursementRequestService.getPendingAdvisorList( req.currentUser, - req.organization, - carNumber + req.organization ); res.status(200).json(requestsPendingAdvisors); } catch (error: unknown) { @@ -316,11 +308,9 @@ export default class ReimbursementRequestsController { static async getAllReimbursementRequests(req: Request, res: Response, next: NextFunction) { try { - const carNumber = req.currentCar?.wbsElement.carNumber; const reimbursementRequests: ReimbursementRequest[] = await ReimbursementRequestService.getAllReimbursementRequests( req.currentUser, - req.organization, - carNumber + req.organization ); res.status(200).json(reimbursementRequests); } catch (error: unknown) { diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 75fe877002..b1624f3343 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -74,11 +74,7 @@ export default class UsersController { static async getUsersFavoriteProjects(req: Request, res: Response, next: NextFunction) { try { - const projects = await UsersService.getUsersFavoriteProjects( - req.currentUser.userId, - req.organization, - req.currentCar?.carId - ); + const projects = await UsersService.getUsersFavoriteProjects(req.currentUser.userId, req.organization); res.status(200).json(projects); } catch (error: unknown) { @@ -106,7 +102,7 @@ export default class UsersController { const { user, token } = await UsersService.logUserIn(idToken, header!); - res.cookie('token', token, { httpOnly: true, sameSite: 'none', secure: true, maxAge: 7 * 24 * 60 * 60 * 1000 }); + res.cookie('token', token, { httpOnly: true, sameSite: 'none', secure: true }); res.status(200).json(user); } catch (error: unknown) { next(error); diff --git a/src/backend/src/controllers/work-packages.controllers.ts b/src/backend/src/controllers/work-packages.controllers.ts index 1d7224e7bc..2c0b34ddf9 100644 --- a/src/backend/src/controllers/work-packages.controllers.ts +++ b/src/backend/src/controllers/work-packages.controllers.ts @@ -7,13 +7,9 @@ export default class WorkPackagesController { // Fetch all work packages, optionally filtered by query parameters static async getAllWorkPackages(req: Request, res: Response, next: NextFunction) { try { - const { status, daysUntilDeadline } = req.query as { status?: string; daysUntilDeadline?: string }; + const { query } = req; - const outputWorkPackages: WorkPackage[] = await WorkPackagesService.getAllWorkPackages( - { status, daysUntilDeadline }, - req.organization, - req.currentCar?.carId - ); + const outputWorkPackages: WorkPackage[] = await WorkPackagesService.getAllWorkPackages(query, req.organization); res.status(200).json(outputWorkPackages); } catch (error: unknown) { @@ -28,8 +24,7 @@ export default class WorkPackagesController { const outputWorkPackages: WorkPackagePreview[] = await WorkPackagesService.getAllWorkPackagesPreview( status, - req.organization, - req.currentCar?.carId + req.organization ); res.status(200).json(outputWorkPackages); @@ -163,8 +158,7 @@ export default class WorkPackagesController { const workPackages: WorkPackagePreview[] = await WorkPackagesService.getHomePageWorkPackages( req.currentUser, req.organization, - selection as WorkPackageSelection, - req.currentCar?.carId + selection as WorkPackageSelection ); res.status(200).json(workPackages); diff --git a/src/backend/src/integrations/slack.ts b/src/backend/src/integrations/slack.ts index 0ac331f125..2005e2a9e0 100644 --- a/src/backend/src/integrations/slack.ts +++ b/src/backend/src/integrations/slack.ts @@ -143,26 +143,6 @@ export const editMessage = async ( } }; -/** - * Deletes a slack message - * @param channelId - the channel id of the channel containing the message - * @param timestamp - the timestamp of the message to delete - */ -export const deleteMessage = async (channelId: string, timestamp: string) => { - const client = getSlackClient(); - if (!client) return; - - try { - await client.chat.delete({ - channel: channelId, - ts: timestamp - }); - } catch (error) { - console.error('Failed to delete Slack message:', (error as any)?.data?.error ?? error); - return undefined; - } -}; - /** * Reacts to a slack message * @param slackId - the channel id of the channel of the message to reply to @@ -271,23 +251,6 @@ export const getChannelName = async (channelId: string) => { } }; -/** - * Checks whether the bot is a member of the given channel - * @param channelId the id of the slack channel - * @returns true if the bot is a member of the channel, false otherwise - */ -export const checkBotInChannel = async (channelId: string): Promise => { - const client = getSlackClient(); - if (!client) return false; - - try { - const channelRes = await client.conversations.info({ channel: channelId }); - return channelRes.channel?.is_member ?? false; - } catch (error) { - return false; - } -}; - /** * Given a slack user id, prood.uces the name of the channel * @param userId the id of the slack user diff --git a/src/backend/src/prisma-query-args/attendance.query-args.ts b/src/backend/src/prisma-query-args/attendance.query-args.ts deleted file mode 100644 index 7c9b5e15f0..0000000000 --- a/src/backend/src/prisma-query-args/attendance.query-args.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { getUserQueryArgs } from './user.query-args.js'; - -export type MeetingAttendanceQueryArgs = ReturnType; -export type MeetingAttendanceWithAttendeesQueryArgs = ReturnType; - -export const getMeetingAttendanceQueryArgs = (organizationId: string) => - Prisma.validator()({ - include: { - userCreated: getUserQueryArgs(organizationId), - team: { - select: { - teamId: true, - teamName: true, - headId: true, - members: { select: { userId: true } }, - leads: { select: { userId: true } } - } - }, - attendees: { select: { userId: true } } - } - }); - -export const getMeetingAttendanceWithAttendeesQueryArgs = (organizationId: string) => - Prisma.validator()({ - include: { - userCreated: getUserQueryArgs(organizationId), - team: { - select: { - teamId: true, - teamName: true, - headId: true, - members: { select: { userId: true } }, - leads: { select: { userId: true } } - } - }, - attendees: getUserQueryArgs(organizationId) - } - }); diff --git a/src/backend/src/prisma-query-args/change-requests.query-args.ts b/src/backend/src/prisma-query-args/change-requests.query-args.ts index ebe4c8be15..abae733c5d 100644 --- a/src/backend/src/prisma-query-args/change-requests.query-args.ts +++ b/src/backend/src/prisma-query-args/change-requests.query-args.ts @@ -68,51 +68,6 @@ export const getManyChangeRequestQueryArgs = (organizationId: string) => } }); -export type ChangeRequestGuestQueryArgs = ReturnType; - -export const getGuestChangeRequestQueryArgs = (organizationId: string) => - Prisma.validator()({ - select: { - crId: true, - identifier: true, - dateSubmitted: true, - type: true, - accepted: true, - dateReviewed: true, - submitter: getUserQueryArgs(organizationId), - reviewer: getUserQueryArgs(organizationId), - changes: { select: { changeId: true } }, - wbsElement: { - select: { - carNumber: true, - projectNumber: true, - workPackageNumber: true, - name: true, - project: { - select: { - wbsElement: { select: { name: true } }, - teams: { - select: { teamType: { select: { name: true } } } - } - } - }, - workPackage: { - select: { - project: { - select: { - wbsElement: { select: { name: true } }, - teams: { - select: { teamType: { select: { name: true } } } - } - } - } - } - } - } - } - } - }); - export const getChangeRequestWithProjectAndWorkPackageQueryArgs = (organizationId: string) => Prisma.validator()({ include: { diff --git a/src/backend/src/prisma-query-args/projects.query-args.ts b/src/backend/src/prisma-query-args/projects.query-args.ts index c7efd06b11..1c430b3c8a 100644 --- a/src/backend/src/prisma-query-args/projects.query-args.ts +++ b/src/backend/src/prisma-query-args/projects.query-args.ts @@ -1,4 +1,4 @@ -import { Prisma, Task_Status } from '@prisma/client'; +import { Prisma } from '@prisma/client'; import { getUserQueryArgs } from './user.query-args.js'; import { getDescriptionBulletQueryArgs } from './description-bullets.query-args.js'; import { getTeamPreviewQueryArgs } from './teams.query-args.js'; @@ -100,12 +100,6 @@ export const getProjectPreviewQueryArgs = (organizationId: string) => abbreviation: true, teams: { select: { - teamType: { - select: { - teamTypeId: true, - name: true - } - }, teamId: true, teamName: true } @@ -129,13 +123,7 @@ export const getProjectOverviewQueryArgs = (organizationId: string) => manager: getUserQueryArgs(organizationId), status: true, links: getLinkQueryArgs(), - _count: { - select: { - tasks: { - where: { AND: [{ dateDeleted: null }, { NOT: { status: Task_Status.DONE } }] } - } - } - } + tasks: getTaskQueryArgs(organizationId) } }, workPackages: getWorkPackagePreviewQueryArgs(), @@ -144,12 +132,6 @@ export const getProjectOverviewQueryArgs = (organizationId: string) => abbreviation: true, teams: { select: { - teamType: { - select: { - teamTypeId: true, - name: true - } - }, teamId: true, teamName: true } diff --git a/src/backend/src/prisma-query-args/team-type.query-args.ts b/src/backend/src/prisma-query-args/team-type.query-args.ts new file mode 100644 index 0000000000..d286302e73 --- /dev/null +++ b/src/backend/src/prisma-query-args/team-type.query-args.ts @@ -0,0 +1,18 @@ +import { Prisma } from '@prisma/client'; + +export type TeamTypeQueryArgs = ReturnType; +export type TeamTypePreviewQueryArgs = ReturnType; + +export const getTeamTypeQueryArgs = () => + Prisma.validator()({ + select: { + name: true + } + }); + +export const getTeamTypePreviewQueryArgs = () => + Prisma.validator()({ + select: { + name: true + } + }); diff --git a/src/backend/src/prisma-query-args/teams.query-args.ts b/src/backend/src/prisma-query-args/teams.query-args.ts index 1d2de0b089..76e866c6c4 100644 --- a/src/backend/src/prisma-query-args/teams.query-args.ts +++ b/src/backend/src/prisma-query-args/teams.query-args.ts @@ -39,6 +39,8 @@ export const getTeamPreviewQueryArgs = (organizationId: string) => members: getUserQueryArgs(organizationId), head: getUserQueryArgs(organizationId), leads: getUserQueryArgs(organizationId), - teamType: true + teamType: { + select: { teamTypeId: true, name: true } + } } }); diff --git a/src/backend/src/prisma/migrations/20260317050642_meeting_attendance/migration.sql b/src/backend/src/prisma/migrations/20260317050642_meeting_attendance/migration.sql deleted file mode 100644 index 56c0ec5578..0000000000 --- a/src/backend/src/prisma/migrations/20260317050642_meeting_attendance/migration.sql +++ /dev/null @@ -1,45 +0,0 @@ --- CreateTable -CREATE TABLE "Meeting_Attendance" ( - "meetingAttendanceId" TEXT NOT NULL, - "organizationId" TEXT NOT NULL, - "teamId" TEXT NOT NULL, - "userCreatedId" TEXT NOT NULL, - "openedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "closedAt" TIMESTAMP(3), - "slackChannelId" TEXT NOT NULL, - "slackMessageTimestamp" TEXT NOT NULL, - - CONSTRAINT "Meeting_Attendance_pkey" PRIMARY KEY ("meetingAttendanceId") -); - --- CreateTable -CREATE TABLE "_meetingAttendees" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL, - - CONSTRAINT "_meetingAttendees_AB_pkey" PRIMARY KEY ("A","B") -); - --- CreateIndex -CREATE INDEX "Meeting_Attendance_organizationId_idx" ON "Meeting_Attendance"("organizationId"); - --- CreateIndex -CREATE INDEX "Meeting_Attendance_teamId_idx" ON "Meeting_Attendance"("teamId"); - --- CreateIndex -CREATE INDEX "_meetingAttendees_B_index" ON "_meetingAttendees"("B"); - --- AddForeignKey -ALTER TABLE "Meeting_Attendance" ADD CONSTRAINT "Meeting_Attendance_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Meeting_Attendance" ADD CONSTRAINT "Meeting_Attendance_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("teamId") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Meeting_Attendance" ADD CONSTRAINT "Meeting_Attendance_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_meetingAttendees" ADD CONSTRAINT "_meetingAttendees_A_fkey" FOREIGN KEY ("A") REFERENCES "Meeting_Attendance"("meetingAttendanceId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_meetingAttendees" ADD CONSTRAINT "_meetingAttendees_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/migrations/20260402230143_bom_improvements_pt2/migration.sql b/src/backend/src/prisma/migrations/20260402230143_bom_improvements_pt2/migration.sql deleted file mode 100644 index 536b672a3b..0000000000 --- a/src/backend/src/prisma/migrations/20260402230143_bom_improvements_pt2/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Material" ADD COLUMN "isCopied" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 47acec4093..9814fa65b0 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -303,8 +303,6 @@ model User { leadershipCrAsLead Leadership_CR[] @relation(name: "leadershipCrLead") leadershipCrAsManager Leadership_CR[] @relation(name: "leadershipCrManager") prospectiveSponsorsContacted Prospective_Sponsor[] @relation(name: "prospectiveSponsorContactor") - createdMeetingAttendances Meeting_Attendance[] @relation(name: "meetingAttendanceCreated") - attendedMeetingAttendances Meeting_Attendance[] @relation(name: "meetingAttendees") } model Role { @@ -341,7 +339,6 @@ model Team { organization Organization @relation(fields: [organizationId], references: [organizationId]) checklists Checklist[] projectTemplates Project_Template[] - meetingAttendances Meeting_Attendance[] @@index([headId]) @@index([organizationId]) @@ -739,11 +736,11 @@ model Receipt { model Reimbursement_Request { reimbursementRequestId String @id @default(uuid()) identifier Int - saboId String? @unique + saboId String? @unique dateCreated DateTime @default(now()) dateDeleted DateTime? dateOfExpense DateTime? - description String @default("") + description String @default("") reimbursementStatuses Reimbursement_Status[] recipientId String recipient User @relation(name: "reimbursementRequestRecipient", fields: [recipientId], references: [userId]) @@ -1020,7 +1017,6 @@ model Material { linkUrl String notes String? reimbursementProducts Reimbursement_Product[] - isCopied Boolean @default(false) @@index([assemblyId]) @@index([materialTypeId]) @@ -1448,7 +1444,6 @@ model Organization { machineries Machinery[] calendars Calendar[] eventTypes Event_Type[] - meetingAttendances Meeting_Attendance[] } model FrequentlyAskedQuestion { @@ -1806,21 +1801,3 @@ model Guest_Definition { @@index([organizationId]) } - -model Meeting_Attendance { - meetingAttendanceId String @id @default(uuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [organizationId]) - teamId String - team Team @relation(fields: [teamId], references: [teamId]) - userCreatedId String - userCreated User @relation(name: "meetingAttendanceCreated", fields: [userCreatedId], references: [userId]) - openedAt DateTime @default(now()) - closedAt DateTime? - slackChannelId String - slackMessageTimestamp String - attendees User[] @relation(name: "meetingAttendees") - - @@index([organizationId]) - @@index([teamId]) -} diff --git a/src/backend/src/prisma/seed-data/reimbursement-requests.seed.ts b/src/backend/src/prisma/seed-data/reimbursement-requests.seed.ts index 64da4be25b..c36f9800ec 100644 --- a/src/backend/src/prisma/seed-data/reimbursement-requests.seed.ts +++ b/src/backend/src/prisma/seed-data/reimbursement-requests.seed.ts @@ -85,7 +85,7 @@ export const seedReimbursementRequests = async ( { name: 'High Performance Battery Pack', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -116,7 +116,7 @@ export const seedReimbursementRequests = async ( { name: 'Development Tools Kit', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -152,7 +152,7 @@ export const seedReimbursementRequests = async ( { name: 'Cloud Storage Subscription', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -193,7 +193,7 @@ export const seedReimbursementRequests = async ( { name: 'Unnecessary Luxury Item', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -224,7 +224,7 @@ export const seedReimbursementRequests = async ( { name: 'Safety Equipment - Helmets', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -239,7 +239,7 @@ export const seedReimbursementRequests = async ( { name: 'Safety Equipment - Gloves', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -275,7 +275,7 @@ export const seedReimbursementRequests = async ( { name: 'Office Supplies', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -316,7 +316,7 @@ export const seedReimbursementRequests = async ( { name: 'Testing Equipment', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -346,7 +346,7 @@ export const seedReimbursementRequests = async ( { name: 'Software Licenses', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -387,7 +387,7 @@ export const seedReimbursementRequests = async ( { name: 'Training Materials', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -423,7 +423,7 @@ export const seedReimbursementRequests = async ( { name: 'Research Database Access', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -464,7 +464,7 @@ export const seedReimbursementRequests = async ( { name: 'Workshop Snacks', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -494,7 +494,7 @@ export const seedReimbursementRequests = async ( { name: 'Sensor Components', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -541,7 +541,7 @@ export const seedReimbursementRequests = async ( { name: 'Emergency Replacement Parts', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -577,7 +577,7 @@ export const seedReimbursementRequests = async ( { name: 'Team Building Materials', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -612,7 +612,7 @@ export const seedReimbursementRequests = async ( { name: 'Learning Resources', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -648,7 +648,7 @@ export const seedReimbursementRequests = async ( { name: 'Presentation Materials', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -689,7 +689,7 @@ export const seedReimbursementRequests = async ( { name: 'Personal Electronics', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -720,7 +720,7 @@ export const seedReimbursementRequests = async ( { name: 'CAD Software License', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -761,7 +761,7 @@ export const seedReimbursementRequests = async ( { name: 'Microcontrollers', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -797,7 +797,7 @@ export const seedReimbursementRequests = async ( { name: 'Video Conferencing Equipment', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -844,7 +844,7 @@ export const seedReimbursementRequests = async ( { name: 'Workshop Cleaning Supplies', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -874,7 +874,7 @@ export const seedReimbursementRequests = async ( { name: 'Hand Tools Set', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -915,7 +915,7 @@ export const seedReimbursementRequests = async ( { name: '3D Printing Filament', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -951,7 +951,7 @@ export const seedReimbursementRequests = async ( { name: 'Carbon Fiber Sheets', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -966,7 +966,7 @@ export const seedReimbursementRequests = async ( { name: 'Epoxy Resin', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -981,7 +981,7 @@ export const seedReimbursementRequests = async ( { name: 'Aluminum Stock', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1028,7 +1028,7 @@ export const seedReimbursementRequests = async ( { name: 'High-Speed Data Acquisition System', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -1058,7 +1058,7 @@ export const seedReimbursementRequests = async ( { name: 'Power Supply Units', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1109,7 +1109,7 @@ export const seedReimbursementRequests = async ( { name: 'Development Software Licenses', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -1160,7 +1160,7 @@ export const seedReimbursementRequests = async ( { name: 'Cloud Computing Credits', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1217,7 +1217,7 @@ export const seedReimbursementRequests = async ( { name: 'Safety Equipment', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1274,7 +1274,7 @@ export const seedReimbursementRequests = async ( { name: 'Tablets for Design Team', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -1342,7 +1342,7 @@ export const seedReimbursementRequests = async ( { name: 'Bulk Workshop Supplies', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1410,7 +1410,7 @@ export const seedReimbursementRequests = async ( { name: 'Battery Testing Equipment', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -1483,7 +1483,7 @@ export const seedReimbursementRequests = async ( { name: 'PCB Manufacturing', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 2, workPackageNumber: 0 }, @@ -1556,7 +1556,7 @@ export const seedReimbursementRequests = async ( { name: 'Team Event Supplies', reason: { - carNumber: 25, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index 6734f879af..146c769d52 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -41,7 +41,6 @@ import OnboardingServices from '../services/onboarding.services.js'; import { dbSeedAllParts, dbSeedAllPartTags } from './seed-data/parts.seed.js'; import FinanceServices from '../services/finance.services.js'; import CalendarService from '../services/calendar.services.js'; -import { allChangeRequestsReviewed } from '../utils/change-requests.utils.js'; const prisma = new PrismaClient(); @@ -300,40 +299,6 @@ const performSeed: () => Promise = async () => { } }); - await prisma.car.create({ - data: { - wbsElement: { - create: { - name: 'NER-24', - carNumber: 24, - projectNumber: 0, - workPackageNumber: 0, - organizationId - } - } - }, - include: { - wbsElement: true - } - }); - - const car25 = await prisma.car.create({ - data: { - wbsElement: { - create: { - name: 'NER-25', - carNumber: 25, - projectNumber: 0, - workPackageNumber: 0, - organizationId - } - } - }, - include: { - wbsElement: true - } - }); - const miles = await prisma.car.create({ data: { wbsElement: { @@ -352,11 +317,11 @@ const performSeed: () => Promise = async () => { }); /** - * Make an initial change request for NER-25 using the wbs of the genesis project + * Make an initial change request for car 1 using the wbs of the genesis project */ const changeRequest1: StandardChangeRequest = await ChangeRequestsService.createStandardChangeRequest( cyborg, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, fergus.wbsElement.projectNumber, fergus.wbsElement.workPackageNumber, CR_Type.OTHER, @@ -641,7 +606,7 @@ const performSeed: () => Promise = async () => { } = await seedProject( thomasEmrax, changeRequest1.crId, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, 'Impact Attenuator', 'Develop rules-compliant impact attenuator', [huskies.teamId], @@ -669,7 +634,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectHuskies2WbsNumber, projectId: projectHuskies2Id } = await seedProject( thomasEmrax, changeRequest1.crId, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, 'Bodywork', 'Develop rules-compliant bodywork', [huskies.teamId], @@ -697,7 +662,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectHuskies3WbsNumber, projectId: projectHuskies3Id } = await seedProject( thomasEmrax, changeRequest1.crId, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, 'Battery Box', 'Develop rules-compliant battery box.', [huskies.teamId], @@ -725,7 +690,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectHuskies4WbsNumber, projectId: projectHuskies4Id } = await seedProject( thomasEmrax, changeRequest1.crId, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, 'Motor Controller Integration', 'Develop rules-compliant motor controller integration.', [huskies.teamId], @@ -758,7 +723,7 @@ const performSeed: () => Promise = async () => { } = await seedProject( thomasEmrax, changeRequest1.crId, - car25.wbsElement.carNumber, + fergus.wbsElement.carNumber, 'Wiring Harness', 'Develop rules-compliant wiring harness.', [slackBotTeam.teamId], @@ -843,7 +808,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectAvatar1WbsNumber, projectId: projectAvatar1Id } = await seedProject( aang, changeRequest1.crId, - car25.wbsElement.carNumber, + 0, 'Appa Plush', 'Manufacture plushes of Appa for moral support.', [avatarBenders.teamId], @@ -872,7 +837,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectJustice1WbsNumber, projectId: projectJustice1Id } = await seedProject( lexLuther, changeRequest1.crId, - car25.wbsElement.carNumber, + 0, 'Laser Cannon Prototype', 'Develop a prototype of a laser cannon for the Justice League', [justiceLeague.teamId], @@ -929,7 +894,7 @@ const performSeed: () => Promise = async () => { const { projectWbsNumber: projectRavens1WbsNumber } = await seedProject( ryanGiggs, changeRequest1.crId, - car25.wbsElement.carNumber, + 0, 'Stadium Renovation', `Renovate the team's stadium to improve fan experience`, [ravens.teamId], @@ -3506,7 +3471,7 @@ const performSeed: () => Promise = async () => { '1', thomasEmrax, { - carNumber: car25.wbsElement.carNumber, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -3520,7 +3485,7 @@ const performSeed: () => Promise = async () => { 'Resistor', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', { - carNumber: car25.wbsElement.carNumber, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -3543,7 +3508,7 @@ const performSeed: () => Promise = async () => { 'Resistor', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', { - carNumber: car25.wbsElement.carNumber, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -3566,7 +3531,7 @@ const performSeed: () => Promise = async () => { 'Resistor', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', { - carNumber: car25.wbsElement.carNumber, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -3593,7 +3558,7 @@ const performSeed: () => Promise = async () => { [thomasEmrax.userId, batman.userId], [superman.userId, wonderwoman.userId], { - carNumber: car25.wbsElement.carNumber, + carNumber: 0, projectNumber: 1, workPackageNumber: 0 }, @@ -5113,61 +5078,6 @@ const performSeed: () => Promise = async () => { undefined, undefined ); - - /* Guest Definitions */ - const guestDef1 = await prisma.guest_Definition.create({ - data: { - term: 'NER', - description: 'A really awesome organization!', - order: 0, - organizationId, - userCreatedId: batman.userId - } - }); - - await RecruitmentServices.createGuestDefinition( - thomasEmrax, - ner, - 'Projects', - 'This is the definition of a project. Projects are blah blah blah', - 0, - 'bar_chart', - 'Click here to view all our projects!', - '/projects' - ); - - await RecruitmentServices.createGuestDefinition( - thomasEmrax, - ner, - 'Change Requests', - 'This is the definiton for a change request. Changes requests are blah blah blah', - 0, - 'bar_chart', - 'Click here to view all our change requests!', - '/change-requests' - ); - - await RecruitmentServices.createGuestDefinition( - thomasEmrax, - ner, - 'Gantt Chart', - 'This is the definiton for a change request. Changes requests are blah blah blah', - 0, - 'bar_chart', - 'Click here to view all our projects!', - '/gantt' - ); - - await RecruitmentServices.createGuestDefinition( - thomasEmrax, - ner, - 'Design Reviews', - 'This is the definiton for a design review. Design reviews are blah blah blah', - 0, - 'bar_chart', - 'Click here to view all our design reviews!', - '/design-reviews' - ); }; performSeed() diff --git a/src/backend/src/routes/attendance.routes.ts b/src/backend/src/routes/attendance.routes.ts deleted file mode 100644 index fca82b1643..0000000000 --- a/src/backend/src/routes/attendance.routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import express from 'express'; -import { body } from 'express-validator'; -import { nonEmptyString, validateInputs } from '../utils/validation.utils.js'; -import AttendanceController from '../controllers/attendance.controllers.js'; - -const attendanceRouter = express.Router(); - -attendanceRouter.post( - '/', - nonEmptyString(body('teamId')), - nonEmptyString(body('message')), - validateInputs, - AttendanceController.takeAttendance -); - -attendanceRouter.get('/', AttendanceController.getAllAttendances); -attendanceRouter.get('/ongoing/:teamId', AttendanceController.getOngoingAttendance); -attendanceRouter.post('/close/:teamId', AttendanceController.closeOngoingAttendance); -attendanceRouter.get('/check-channel/:teamId', AttendanceController.checkChannel); -attendanceRouter.get('/:meetingAttendanceId', AttendanceController.getAttendanceById); - -export default attendanceRouter; diff --git a/src/backend/src/routes/calendar.routes.ts b/src/backend/src/routes/calendar.routes.ts index 8eb5e6a94e..6f7e60ce12 100644 --- a/src/backend/src/routes/calendar.routes.ts +++ b/src/backend/src/routes/calendar.routes.ts @@ -297,6 +297,5 @@ calendarRouter.post( ); calendarRouter.get('/calendars', CalendarController.getAllCalendars); -calendarRouter.post('/events-paginated', CalendarController.getAllEventsPaginated); export default calendarRouter; diff --git a/src/backend/src/routes/change-requests.routes.ts b/src/backend/src/routes/change-requests.routes.ts index 744f4f615c..89b3fe0c19 100644 --- a/src/backend/src/routes/change-requests.routes.ts +++ b/src/backend/src/routes/change-requests.routes.ts @@ -14,7 +14,6 @@ import { const changeRequestsRouter = express.Router(); changeRequestsRouter.get('/', ChangeRequestsController.getAllChangeRequests); -changeRequestsRouter.get('/guest', ChangeRequestsController.getAllGuestChangeRequests); changeRequestsRouter.get('/to-review', ChangeRequestsController.getToReviewChangeRequests); changeRequestsRouter.get('/unreviewed', ChangeRequestsController.getUnreviewedChangeRequests); diff --git a/src/backend/src/routes/recruitment.routes.ts b/src/backend/src/routes/recruitment.routes.ts index a2123d7a3f..2ba260e479 100644 --- a/src/backend/src/routes/recruitment.routes.ts +++ b/src/backend/src/routes/recruitment.routes.ts @@ -50,10 +50,8 @@ recruitmentRouter.post( recruitmentRouter.delete('/faq/:faqId/delete', RecruitmentController.deleteFaq); -/* Guest Definition Section */ - recruitmentRouter.post( - '/guestdefinition/create', + '/guestDefinition/create', nonEmptyString(body('term')), nonEmptyString(body('description')), body('order').isInt(), @@ -64,7 +62,4 @@ recruitmentRouter.post( RecruitmentController.createGuestDefinition ); -recruitmentRouter.delete('/guestdefinition/:definitionId/delete', RecruitmentController.deleteGuestDefinition); -recruitmentRouter.get('/guestdefinitions', RecruitmentController.getAllGuestDefintions); - export default recruitmentRouter; diff --git a/src/backend/src/routes/slack.routes.ts b/src/backend/src/routes/slack.routes.ts index 1863e87869..c152ab664f 100644 --- a/src/backend/src/routes/slack.routes.ts +++ b/src/backend/src/routes/slack.routes.ts @@ -1,6 +1,5 @@ import { getSlackApp } from '../integrations/slack.js'; import SlackController from '../controllers/slack.controllers.js'; -import AttendanceService from '../services/attendance.services.js'; // Register Slack event listeners only if the Slack app is configured const slackApp = getSlackApp(); @@ -15,18 +14,6 @@ if (slackApp) { console.error(error); } }); - - // Register reaction_added event listener for attendance tracking - slackApp.event('reaction_added', async ({ event, logger }: any) => { - try { - const { user, item } = event; - if (item.type !== 'message') return; - await AttendanceService.handleReactionAdded(user, item.channel, item.ts); - } catch (error) { - logger.error('Error handling reaction_added event:', error); - console.error(error); - } - }); } /** diff --git a/src/backend/src/routes/work-packages.routes.ts b/src/backend/src/routes/work-packages.routes.ts index b3aa8ac529..328bcad2a5 100644 --- a/src/backend/src/routes/work-packages.routes.ts +++ b/src/backend/src/routes/work-packages.routes.ts @@ -62,9 +62,7 @@ workPackagesRouter.post( WorkPackagesController.editWorkPackage ); workPackagesRouter.delete('/:wbsNum/delete', WorkPackagesController.deleteWorkPackage); - workPackagesRouter.get('/:wbsNum/blocking', WorkPackagesController.getBlockingWorkPackages); - workPackagesRouter.post( '/slack-upcoming-deadlines', isDateOnly(body('deadline')), diff --git a/src/backend/src/services/attendance.services.ts b/src/backend/src/services/attendance.services.ts deleted file mode 100644 index 596dedc3f1..0000000000 --- a/src/backend/src/services/attendance.services.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { Organization } from '@prisma/client'; -import { MeetingAttendance, MeetingAttendanceWithAttendees, RoleEnum, User, isAtLeastRank } from 'shared'; -import prisma from '../prisma/prisma.js'; -import { AccessDeniedException, HttpException, NotFoundException } from '../utils/errors.utils.js'; -import { - getMeetingAttendanceQueryArgs, - getMeetingAttendanceWithAttendeesQueryArgs -} from '../prisma-query-args/attendance.query-args.js'; -import { - meetingAttendanceTransformer, - meetingAttendanceWithAttendeesTransformer -} from '../transformers/attendance.transformer.js'; -import { - checkBotInChannel, - editMessage, - getChannelName, - replyToMessageInThread, - sendMessage -} from '../integrations/slack.js'; -import { userHasPermission } from '../utils/users.utils.js'; - -export default class AttendanceService { - static async takeAttendance( - submitter: User, - teamId: string, - message: string, - organization: Organization - ): Promise { - const team = await prisma.team.findUnique({ - where: { teamId }, - include: { members: true } - }); - - if (!team) throw new NotFoundException('Team', teamId); - if (team.organizationId !== organization.organizationId) throw new NotFoundException('Team', teamId); - - if ( - !(await userHasPermission(submitter.userId, organization.organizationId, (role) => - isAtLeastRank(RoleEnum.ADMIN, role) - )) && - submitter.userId !== team.headId - ) { - throw new AccessDeniedException('Only team heads or admins can take attendance'); - } - - const openAttendance = await prisma.meeting_Attendance.findFirst({ - where: { teamId, closedAt: null, organizationId: organization.organizationId } - }); - - if (openAttendance) { - throw new HttpException(400, 'There is already an open attendance session for this team'); - } - - const result = await sendMessage(team.slackId, message); - if (!result) { - throw new HttpException( - 500, - 'Failed to send Slack message. Check that the team Slack ID is valid and the bot is in the channel.' - ); - } - - const attendance = await prisma.meeting_Attendance.create({ - data: { - organizationId: organization.organizationId, - teamId, - userCreatedId: submitter.userId, - slackChannelId: result.channelId, - slackMessageTimestamp: result.ts - }, - ...getMeetingAttendanceQueryArgs(organization.organizationId) - }); - - setTimeout(() => AttendanceService.closeAttendance(attendance.meetingAttendanceId), 3600000); - - return meetingAttendanceTransformer(attendance); - } - - static async getAttendanceById( - meetingAttendanceId: string, - organization: Organization - ): Promise { - const attendance = await prisma.meeting_Attendance.findUnique({ - where: { meetingAttendanceId }, - ...getMeetingAttendanceWithAttendeesQueryArgs(organization.organizationId) - }); - - if (!attendance || attendance.organizationId !== organization.organizationId) { - throw new NotFoundException('Meeting Attendance', meetingAttendanceId); - } - - return meetingAttendanceWithAttendeesTransformer(attendance); - } - - static async getAllAttendances(organization: Organization): Promise { - const attendances = await prisma.meeting_Attendance.findMany({ - where: { organizationId: organization.organizationId }, - ...getMeetingAttendanceQueryArgs(organization.organizationId), - orderBy: { openedAt: 'desc' } - }); - - return attendances.map(meetingAttendanceTransformer); - } - - static async handleReactionAdded(slackUserId: string, channelId: string, messageTimestamp: string): Promise { - const attendance = await prisma.meeting_Attendance.findFirst({ - where: { slackChannelId: channelId, slackMessageTimestamp: messageTimestamp, closedAt: null } - }); - - if (!attendance) return; - - const userWithSettings = await prisma.user.findFirst({ - where: { - userSettings: { slackId: slackUserId }, - organizations: { some: { organizationId: attendance.organizationId } } - } - }); - - if (!userWithSettings) { - await replyToMessageInThread( - channelId, - messageTimestamp, - `<@${slackUserId}> Your Slack ID is not linked to a FinishLine account. Please set your Slack ID in your profile settings to be counted for attendance.` - ); - return; - } - - await prisma.meeting_Attendance.update({ - where: { meetingAttendanceId: attendance.meetingAttendanceId }, - data: { - attendees: { connect: { userId: userWithSettings.userId } } - } - }); - } - - static async closeAttendance(meetingAttendanceId: string): Promise { - const attendance = await prisma.meeting_Attendance.findUnique({ - where: { meetingAttendanceId }, - include: { - team: { - select: { - headId: true, - members: { select: { userId: true } }, - leads: { select: { userId: true } } - } - }, - attendees: { select: { userId: true } } - } - }); - - if (!attendance || attendance.closedAt) return; - - const teamMemberIds = new Set([ - ...attendance.team.members.map((m) => m.userId), - ...attendance.team.leads.map((l) => l.userId), - attendance.team.headId - ]); - const attendeeIds = new Set(attendance.attendees.map((a) => a.userId)); - const attendeesCount = attendance.attendees.length; - const teamMemberAttendees = [...teamMemberIds].filter((id) => attendeeIds.has(id)).length; - const teamMemberPercent = teamMemberIds.size > 0 ? (teamMemberAttendees / teamMemberIds.size) * 100 : 0; - - const closedMessage = `Attendance is now closed. ${attendeesCount} attended (${teamMemberPercent.toFixed(1)}% of team).`; - await editMessage(attendance.slackChannelId, attendance.slackMessageTimestamp, closedMessage); - - await prisma.meeting_Attendance.update({ - where: { meetingAttendanceId }, - data: { closedAt: new Date() } - }); - } - - static async getOngoingAttendance(teamId: string, organization: Organization): Promise { - const team = await prisma.team.findUnique({ where: { teamId } }); - - if (!team || team.organizationId !== organization.organizationId) { - throw new NotFoundException('Team', teamId); - } - - const attendance = await prisma.meeting_Attendance.findFirst({ - where: { teamId, closedAt: null, organizationId: organization.organizationId }, - ...getMeetingAttendanceQueryArgs(organization.organizationId) - }); - - return attendance ? meetingAttendanceTransformer(attendance) : null; - } - - static async closeOngoingAttendance(teamId: string, submitter: User, organization: Organization): Promise { - const team = await prisma.team.findUnique({ where: { teamId } }); - - if (!team || team.organizationId !== organization.organizationId) { - throw new NotFoundException('Team', teamId); - } - - if ( - !(await userHasPermission(submitter.userId, organization.organizationId, (role) => - isAtLeastRank(RoleEnum.ADMIN, role) - )) && - submitter.userId !== team.headId - ) { - throw new AccessDeniedException('Only team heads or admins can close attendance'); - } - - const openAttendance = await prisma.meeting_Attendance.findFirst({ - where: { teamId, closedAt: null, organizationId: organization.organizationId } - }); - - if (!openAttendance) { - throw new HttpException(400, 'There is no open attendance session for this team'); - } - - await AttendanceService.closeAttendance(openAttendance.meetingAttendanceId); - } - - static async checkTeamChannel( - teamId: string, - organization: Organization - ): Promise<{ channelName: string | undefined; valid: boolean }> { - const team = await prisma.team.findUnique({ where: { teamId } }); - - if (!team || team.organizationId !== organization.organizationId) { - throw new NotFoundException('Team', teamId); - } - - const channelName = await getChannelName(team.slackId); - const botInChannel = channelName ? await checkBotInChannel(team.slackId) : false; - return { channelName, valid: !!channelName && botInChannel }; - } -} diff --git a/src/backend/src/services/boms.services.ts b/src/backend/src/services/boms.services.ts index 324e9d3cdf..a7f434267f 100644 --- a/src/backend/src/services/boms.services.ts +++ b/src/backend/src/services/boms.services.ts @@ -204,8 +204,7 @@ export default class BillOfMaterialsService { dateCreated: new Date(), userCreatedId: user.userId, wbsElementId: destinationProject.wbsElementId, - assemblyId: null, - isCopied: true + assemblyId: null }, ...getMaterialQueryArgs(organization.organizationId) }); diff --git a/src/backend/src/services/calendar.services.ts b/src/backend/src/services/calendar.services.ts index 0d7ad9b64f..7aa1033af7 100644 --- a/src/backend/src/services/calendar.services.ts +++ b/src/backend/src/services/calendar.services.ts @@ -15,8 +15,7 @@ import { Machinery, ScheduleSlot, notGuest, - isSameDay, - EventInstance + isSameDay } from 'shared'; import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js'; import { getEventTypeQueryArgs } from '../prisma-query-args/event-type.query-args.js'; @@ -2750,56 +2749,4 @@ export default class CalendarService { }); return eventTypes.map(eventTypeTransformer); } - - /** - * Gets all the events paginated, ordered by start time and grouped by date - * @param organization the org the user is currently in - * @param cursor the start time of the last event on the prev page - * @param pageSize the number of events to return per page - * @returns - */ - static async getAllEventsPaginated( - organization: Organization, - cursor?: Date, - pageSize: number = 25 - ): Promise<{ instances: EventInstance[]; nextCursor: Date | null }> { - const now = new Date(); - - const slots = await prisma.schedule_Slot.findMany({ - where: { - startTime: { - lt: cursor ?? now - }, - event: { - dateDeleted: null, - status: Event_Status.SCHEDULED, - eventType: { - organizationId: organization.organizationId - } - } - }, - include: { - event: getEventQueryArgs(organization.organizationId) - }, - orderBy: { startTime: 'desc' }, - take: pageSize - }); - - const nextCursor = slots.length === pageSize ? slots[slots.length - 1].startTime : null; - - const instances: EventInstance[] = slots.map((slot) => { - const { scheduledTimes, ...eventWithoutSlots } = eventTransformer(slot.event); - return { - ...eventWithoutSlots, - scheduleSlotId: slot.scheduleSlotId, - startTime: slot.startTime, - endTime: slot.endTime, - allDay: slot.allDay, - recurring: slot.event.scheduledTimes.length > 1, - totalScheduledSlots: slot.event.scheduledTimes.length - }; - }); - - return { instances, nextCursor }; - } } diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 8afab03ff5..27202c9c16 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -28,10 +28,7 @@ import { DeletedException, InvalidOrganizationException } from '../utils/errors.utils.js'; -import changeRequestTransformer, { - changeRequestManyTransformer, - guestChangeRequestTransformer -} from '../transformers/change-requests.transformer.js'; +import changeRequestTransformer, { changeRequestManyTransformer } from '../transformers/change-requests.transformer.js'; import { allChangeRequestsReviewed, validateProposedChangesFields, @@ -58,13 +55,11 @@ import { ChangeRequestWithProjectAndWorkPackageQueryArgs, getChangeRequestQueryArgs, getChangeRequestWithProjectAndWorkPackageQueryArgs, - getGuestChangeRequestQueryArgs, getManyChangeRequestQueryArgs } from '../prisma-query-args/change-requests.query-args.js'; import proposedSolutionTransformer from '../transformers/proposed-solutions.transformer.js'; import { getProposedSolutionQueryArgs } from '../prisma-query-args/proposed-solutions.query-args.js'; import { sendCrRequestReviewPopUp, sendCrReviewedPopUp } from '../utils/pop-up.utils.js'; -import { GuestChangeRequest } from '../../../shared/src/types/change-request-types.js'; export default class ChangeRequestsService { /** @@ -93,33 +88,15 @@ export default class ChangeRequestsService { * @param organization The organization the user is currently in * @returns All of the change requests */ - static async getAllChangeRequests(organization: Organization, carId?: string): Promise { + static async getAllChangeRequests(organization: Organization): Promise { const changeRequests = await prisma.change_Request.findMany({ - where: { - dateDeleted: null, - organizationId: organization.organizationId, - ...(carId && { wbsElement: { OR: [{ project: { carId } }, { workPackage: { project: { carId } } }] } }) - }, + where: { dateDeleted: null, organizationId: organization.organizationId }, ...getManyChangeRequestQueryArgs(organization.organizationId) }); return changeRequests.map(changeRequestManyTransformer); } - /** - * gets all the change requests in the database for the given organization, tailored to the guest cr page - * @param organization The organization the user is currently in - * @returns All of the change requests - */ - static async getAllGuestChangeRequests(organization: Organization): Promise { - const changeRequests = await prisma.change_Request.findMany({ - where: { dateDeleted: null, organizationId: organization.organizationId }, - ...getGuestChangeRequestQueryArgs(organization.organizationId) - }); - - return changeRequests.map(guestChangeRequestTransformer); - } - /** * Gets a users change requests that they have been requested reviewer for or, if they are leadership, their teams change requests as well * @@ -127,7 +104,7 @@ export default class ChangeRequestsService { * @param organization The organization the user is in * @returns The user's change requests for them to review */ - static async getToReviewChangeRequests(user: User, organization: Organization, carId?: string): Promise { + static async getToReviewChangeRequests(user: User, organization: Organization): Promise { const wbsOr: Prisma.WBS_ElementWhereInput[] = [{ managerId: user.userId }, { leadId: user.userId }]; if (await userHasPermission(user.userId, organization.organizationId, isLeadership)) { @@ -171,8 +148,7 @@ export default class ChangeRequestsService { }, { NOT: [{ scopeChangeRequest: null }, { submitterId: user.userId }] - }, - ...(carId ? [{ wbsElement: { OR: [{ project: { carId } }, { workPackage: { project: { carId } } }] } }] : []) + } ], organizationId: organization.organizationId, OR: queryOr @@ -194,8 +170,7 @@ export default class ChangeRequestsService { static async getUnreviewedChangeRequests( user: User, wbsnum: WbsNumber | undefined, - organization: Organization, - carId?: string + organization: Organization ): Promise { // Check that its unreviewed and a scope change request, omit activation and stage gate const queryAnd: Prisma.Change_RequestWhereInput[] = [ @@ -208,12 +183,7 @@ export default class ChangeRequestsService { ]; if (wbsnum) queryAnd.push({ wbsElementId: (await validateWbsElement(wbsnum, organization)).wbsElementId }); - else { - queryAnd.push({ submitterId: user.userId }); - queryAnd.push( - ...(carId ? [{ wbsElement: { OR: [{ project: { carId } }, { workPackage: { project: { carId } } }] } }] : []) - ); - } + else queryAnd.push({ submitterId: user.userId }); const changeRequests = await prisma.change_Request.findMany({ where: { @@ -238,17 +208,13 @@ export default class ChangeRequestsService { static async getApprovedChangeRequests( user: User, wbsnum: WbsNumber | undefined, - organization: Organization, - carId?: string + organization: Organization ): Promise { const currentDate = new Date(); const fiveDaysAgo = new Date(currentDate.getTime() - 1000 * 60 * 60 * 24 * 5); // Change requests that were reviewed less than five days ago const queryAnd = wbsnum ? [{ wbsElementId: (await validateWbsElement(wbsnum, organization)).wbsElementId }] - : [ - { submitterId: user.userId }, - ...(carId ? [{ wbsElement: { OR: [{ project: { carId } }, { workPackage: { project: { carId } } }] } }] : []) - ]; + : [{ submitterId: user.userId }]; const changeRequests = await prisma.change_Request.findMany({ where: { diff --git a/src/backend/src/services/finance.services.ts b/src/backend/src/services/finance.services.ts index 1740bf7075..1f31fcd1b3 100644 --- a/src/backend/src/services/finance.services.ts +++ b/src/backend/src/services/finance.services.ts @@ -520,10 +520,6 @@ export default class FinanceServices { return data; } - // Finance data filters by carNumber (integer) rather than carId (UUID) because the - // finance schema links through WbsElement, which owns carNumber directly. Filtering - // by carId would require nested Prisma joins across every finance utility function. - // carNumber is sourced from req.currentCar?.wbsElement.carNumber via middleware. static async getReimbursementRequestTeamData( organization: Organization, teamId: string, @@ -1143,12 +1139,7 @@ export default class FinanceServices { return data; } - static async getSpendingBarCategoryData( - organization: Organization, - startDate?: Date, - endDate?: Date, - carNumber?: number - ): Promise { + static async getSpendingBarCategoryData(organization: Organization): Promise { const { organizationId } = organization; const otherReasons = await prisma.reimbursement_Product_Other_Reason.findMany({ where: { @@ -1160,13 +1151,7 @@ export default class FinanceServices { }); const spendingInfoPromises = otherReasons.map((r) => - this.getReimbursementRequestCategoryData( - r.otherReimbursementProductReasonId, - organization, - startDate, - endDate, - carNumber - ) + this.getReimbursementRequestCategoryData(r.otherReimbursementProductReasonId, organization) ); const spendingInfos = await Promise.all(spendingInfoPromises); diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index 9cbd1d6d3c..ba43a40471 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -3,13 +3,11 @@ import { TaskWithAssignees, endOfDayTomorrow, startOfDayTomorrow, - startOfTodayEST, - startOfTomorrowEST, usersToSlackPings, EventWithAttendees } from '../utils/notifications.utils.js'; import { sendMessage } from '../integrations/slack.js'; -import { daysBetween, wbsPipe, formatTimeForSlack } from 'shared'; +import { daysBetween, startOfDay, wbsPipe, formatTimeForSlack } from 'shared'; import { buildDueString, sendThreadResponse } from '../utils/slack.utils.js'; import WorkPackagesService from './work-packages.services.js'; import { addWeeksToDate } from 'shared'; @@ -32,7 +30,7 @@ export default class NotificationsService { static async sendTaskDeadlineSlackNotifications() { const endOfDay = endOfDayTomorrow(); - if (endOfDay.getUTCDay() === 0 || endOfDay.getUTCDay() === 2 || endOfDay.getUTCDay() === 4) return; + if (endOfDay.getDay() === 0 || endOfDay.getDay() === 2 || endOfDay.getDay() === 4) return; const tasks = await prisma.task.findMany({ where: { @@ -83,8 +81,7 @@ export default class NotificationsService { const messageBlock = tasks .map((task) => { // prisma call earlier allows the forced unwrap (deadline is guaranteed to be a non-null value) - const todayMidnightUTC = new Date(new Date().setUTCHours(0, 0, 0, 0)); - const daysUntilDeadline = daysBetween(task.deadline!, todayMidnightUTC); + const daysUntilDeadline = daysBetween(task.deadline!, new Date()); return `${usersToSlackPings(task.assignees ?? [])} (); + const desginReviewEventTeamMap = new Map(); events.forEach((event) => { - // Collect unique team Slack IDs: first from teams directly on the event, then from work packages + // Get all unique teams from all work packages associated with this event const teamSlackIds = new Set(); - event.teams.forEach((team) => { - if (team.slackId) { - teamSlackIds.add(team.slackId); - } - }); - event.workPackages.forEach((workPackage) => { workPackage.project.teams.forEach((team) => { if (team.slackId) { @@ -182,7 +171,7 @@ export default class NotificationsService { }); teamSlackIds.forEach((teamSlackId) => { - const currentEvents = eventTeamMap.get(teamSlackId); + const currentEvents = desginReviewEventTeamMap.get(teamSlackId); const eventWithAttendees = { ...event, attendees: event.requiredMembers.concat(event.optionalMembers).concat(event.userCreated), @@ -192,20 +181,20 @@ export default class NotificationsService { if (currentEvents) { currentEvents.push(eventWithAttendees); } else { - eventTeamMap.set(teamSlackId, [eventWithAttendees]); + desginReviewEventTeamMap.set(teamSlackId, [eventWithAttendees]); } }); }); - // Send the notifications to each team for their respective events - const promises = Array.from(eventTeamMap).map(async ([slackId, events]) => { + // Send the notifications to each team for their respective design reviews + const promises = Array.from(desginReviewEventTeamMap).map(async ([slackId, events]) => { const messageBlock = events .map((event) => { const zoomLink = event.zoomLink ? `<${event.zoomLink}|Zoom Link>\n` : ''; const questionDocLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Question Doc Link>\n` : ''; + // Get work package names for this event const workPackageNames = event.workPackages.map((wp) => wp.wbsElement.name).join(', '); - const workPackagesPart = workPackageNames ? ` (${workPackageNames})` : ''; // Get the earliest scheduled start time for display const [earliestSlot] = event.scheduledTimes @@ -214,7 +203,7 @@ export default class NotificationsService { const timeDisplay = earliestSlot ? formatTimeForSlack(new Date(earliestSlot.startTime!)) : 'TBD'; return ( - `${usersToSlackPings(event.attendees ?? [])} *${event.eventType.name}*: ${event.title}${workPackagesPart} ` + + `${usersToSlackPings(event.attendees ?? [])} ${event.title} (${workPackageNames}) ` + `will be having an event today at ${timeDisplay} ET! ` + zoomLink + questionDocLink @@ -222,9 +211,9 @@ export default class NotificationsService { }) .join('\n\n'); - // messageBlock will be empty if there are events with no attendees + // messageBlock will be empty if there are design reviews with no attendees if (messageBlock !== '') - await sendMessage(slackId, ':calendar: :clock9: Upcoming Events! :clock9: :calendar: \n\n\n' + messageBlock); + await sendMessage(slackId, ':calendar: :clock9: Upcoming Design Reviews! :clock9: :calendar: \n\n\n' + messageBlock); }); await Promise.all(promises); @@ -234,7 +223,7 @@ export default class NotificationsService { * Sends the sponsor task slack notifications for all tasks with a notify date of today */ static async sendSponsorTaskNotifications() { - const startOfToday = new Date(new Date().setUTCHours(0, 0, 0, 0)); + const startOfToday = startOfDay(new Date()); const endOfToday = startOfDayTomorrow(); const sponsorTasks = await prisma.sponsor_Task.findMany({ diff --git a/src/backend/src/services/organizations.services.ts b/src/backend/src/services/organizations.services.ts index 664970cfc9..5891e76072 100644 --- a/src/backend/src/services/organizations.services.ts +++ b/src/backend/src/services/organizations.services.ts @@ -410,7 +410,7 @@ export default class OrganizationsService { throw new NotFoundException('Organization', organizationId); } - return organization.featuredProjects.filter((p) => !p.wbsElement.dateDeleted).map(projectPreviewTransformer); + return organization.featuredProjects.map(projectPreviewTransformer); } /** diff --git a/src/backend/src/services/part-review.services.ts b/src/backend/src/services/part-review.services.ts index 6e25f9fce6..857287b8f6 100644 --- a/src/backend/src/services/part-review.services.ts +++ b/src/backend/src/services/part-review.services.ts @@ -31,7 +31,7 @@ import { getPartReviewRequestQueryArgs, getPartSubmissionQueryArgs } from '../prisma-query-args/part-review.query-args.js'; -import { faqTransformer } from '../transformers/recruitment-transformer.js'; +import { faqTransformer } from '../transformers/faq.transformer.js'; import { partReviewRequestTransformer, partsReviewCommonMistakeTransformer, diff --git a/src/backend/src/services/projects.services.ts b/src/backend/src/services/projects.services.ts index 5267a81529..f0684d5885 100644 --- a/src/backend/src/services/projects.services.ts +++ b/src/backend/src/services/projects.services.ts @@ -47,12 +47,11 @@ export default class ProjectsService { /** * Get all the non deleted projects in the database for the given organization * @param organization the organization the user is currently in - * @param carId optional car id to filter projects by * @returns all the projects with query args for use in the gantt chart */ - static async getAllProjectsGantt(organization: Organization, carId?: string): Promise { + static async getAllProjectsGantt(organization: Organization): Promise { const projects = await prisma.project.findMany({ - where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId }, ...(carId && { carId }) }, + where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId } }, ...getProjectGanttQueryArgs(organization.organizationId) }); @@ -62,12 +61,11 @@ export default class ProjectsService { /** * Get all projects for given organization * @param organization the organization the user is in - * @param carId optional car id to filter projects by * @returns all the projects with preview query args */ - static async getAllProjects(organization: Organization, carId?: string): Promise { + static async getAllProjects(organization: Organization): Promise { const projects = await prisma.project.findMany({ - where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId }, ...(carId && { carId }) }, + where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId } }, orderBy: { wbsElement: { dateCreated: 'desc' } }, ...getProjectPreviewQueryArgs(organization.organizationId) }); @@ -79,18 +77,16 @@ export default class ProjectsService { * Get all projects that the user is the lead or manager of * @param user the user making the request * @param organization the oranization the user is in - * @param carId optional car id to filter projects by * @returns the projects the user is a lead or manager of with preview query args */ - static async getUsersLeadingProjects(user: User, organization: Organization, carId?: string): Promise { + static async getUsersLeadingProjects(user: User, organization: Organization): Promise { const projects = await prisma.project.findMany({ where: { wbsElement: { organizationId: organization.organizationId, dateDeleted: null, OR: [{ leadId: user.userId }, { managerId: user.userId }] - }, - ...(carId && { carId }) + } }, ...getProjectOverviewQueryArgs(organization.organizationId) }); @@ -102,10 +98,9 @@ export default class ProjectsService { * Get all projects related to teams the user is on * @param user the user making the request * @param organization the organization the user is in - * @param carId optional car id to filter projects by * @returns all projects associated with teams the user is on with overview card query args */ - static async getUsersTeamsProjects(user: User, organization: Organization, carId?: string): Promise { + static async getUsersTeamsProjects(user: User, organization: Organization): Promise { const projects = await prisma.project.findMany({ where: { wbsElement: { @@ -134,8 +129,7 @@ export default class ProjectsService { } ] } - }, - ...(carId && { carId }) + } }, ...getProjectOverviewQueryArgs(organization.organizationId) }); @@ -147,10 +141,9 @@ export default class ProjectsService { * Get the projects for a given team * @param organization * @param teamId - * @param carId optional car id to filter projects by * @returns all the projects for the given team with full project query args */ - static async getTeamsProjects(organization: Organization, teamId: string, carId?: string): Promise { + static async getTeamsProjects(organization: Organization, teamId: string): Promise { const projects = await prisma.project.findMany({ where: { wbsElement: { @@ -161,8 +154,7 @@ export default class ProjectsService { some: { teamId } - }, - ...(carId && { carId }) + } }, ...getProjectQueryArgs(organization.organizationId) }); diff --git a/src/backend/src/services/recruitment.services.ts b/src/backend/src/services/recruitment.services.ts index 9c494c0721..4c7d978c61 100644 --- a/src/backend/src/services/recruitment.services.ts +++ b/src/backend/src/services/recruitment.services.ts @@ -3,7 +3,7 @@ import { isAdmin, User } from 'shared'; import prisma from '../prisma/prisma.js'; import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../utils/errors.utils.js'; import { userHasPermission } from '../utils/users.utils.js'; -import { faqTransformer, guestDefinitionTransformer } from '../transformers/recruitment-transformer.js'; +import { faqTransformer } from '../transformers/faq.transformer.js'; import { getFaqQueryArgs } from '../prisma-query-args/faq.query-args.js'; export default class RecruitmentServices { @@ -251,35 +251,6 @@ export default class RecruitmentServices { } }); - return guestDefinitionTransformer(definition); - } - - static async getAllGuestDefinitions(organization: Organization) { - const allGuestDefintions = await prisma.guest_Definition.findMany({ - where: { organizationId: organization.organizationId, dateDeleted: null } - }); - - return allGuestDefintions.map(guestDefinitionTransformer); - } - - /** - * Deletes a guestDefinition with the given organization Id and definitionId - * @param deleter the user requesting to delete the guestDefinition - * @param organizationId the organization ID of the deleter - */ - static async deleteGuestDefinition(deleter: User, definitionId: string, organization: Organization): Promise { - if (!(await userHasPermission(deleter.userId, organization.organizationId, isAdmin))) { - throw new AccessDeniedAdminOnlyException('delete a guestDefinition'); - } - - const def = await prisma.guest_Definition.findUnique({ where: { definitionId } }); - - if (!def) throw new NotFoundException('Guest Definition', definitionId); - if (def.dateDeleted) throw new DeletedException('Guest Definition', definitionId); - - await prisma.guest_Definition.update({ - where: { definitionId }, - data: { dateDeleted: new Date(), userDeletedId: deleter.userId } - }); + return definition; } } diff --git a/src/backend/src/services/reimbursement-requests.services.ts b/src/backend/src/services/reimbursement-requests.services.ts index f5d1b779fe..ceda036d10 100644 --- a/src/backend/src/services/reimbursement-requests.services.ts +++ b/src/backend/src/services/reimbursement-requests.services.ts @@ -85,23 +85,9 @@ export default class ReimbursementRequestService { * @param recipient The user retrieving their reimbursement requests * @param organizationId The organization the user is currently in */ - static async getUserReimbursementRequests( - recipient: User, - organization: Organization, - carNumber?: number - ): Promise { + static async getUserReimbursementRequests(recipient: User, organization: Organization): Promise { const userReimbursementRequests = await prisma.reimbursement_Request.findMany({ - where: { - dateDeleted: null, - recipientId: recipient.userId, - organizationId: organization.organizationId, - ...(carNumber !== undefined && - carNumber !== null && { - reimbursementProducts: { - some: { reimbursementProductReason: { wbsElement: { carNumber } } } - } - }) - }, + where: { dateDeleted: null, recipientId: recipient.userId, organizationId: organization.organizationId }, ...getReimbursementRequestQueryArgs(organization.organizationId) }); return userReimbursementRequests.map(reimbursementRequestTransformer); @@ -115,21 +101,10 @@ export default class ReimbursementRequestService { */ static async getUserAssignedReimbursementRequests( assignee: User, - organization: Organization, - carNumber?: number + organization: Organization ): Promise { const assignedReimbursementRequests = await prisma.reimbursement_Request.findMany({ - where: { - dateDeleted: null, - assigneeId: assignee.userId, - organizationId: organization.organizationId, - ...(carNumber !== undefined && - carNumber !== null && { - reimbursementProducts: { - some: { reimbursementProductReason: { wbsElement: { carNumber } } } - } - }) - }, + where: { dateDeleted: null, assigneeId: assignee.userId, organizationId: organization.organizationId }, ...getReimbursementRequestQueryArgs(organization.organizationId) }); return assignedReimbursementRequests.map(reimbursementRequestTransformer); @@ -142,8 +117,7 @@ export default class ReimbursementRequestService { */ static async getUsersTeamsReimbursementRequests( recipient: User, - organization: Organization, - carNumber?: number + organization: Organization ): Promise { const teams = await prisma.team.findMany({ where: { @@ -186,13 +160,7 @@ export default class ReimbursementRequestService { where: { dateDeleted: null, recipientId: { in: Array.from(teamUserIds) }, - organizationId: organization.organizationId, - ...(carNumber !== undefined && - carNumber !== null && { - reimbursementProducts: { - some: { reimbursementProductReason: { wbsElement: { carNumber } } } - } - }) + organizationId: organization.organizationId }, ...getReimbursementRequestQueryArgs(organization.organizationId) }); @@ -325,6 +293,7 @@ export default class ReimbursementRequestService { await sendReimbursementRequestCreatedNotificationAndCreateMessageInfo( createdReimbursementRequest.reimbursementRequestId, + createdReimbursementRequest.identifier, recipient.userId, organization.organizationId ); @@ -458,6 +427,17 @@ export default class ReimbursementRequestService { //set any deleted receipts with a dateDeleted await removeDeletedReceiptPictures(receiptPictures, oldReimbursementRequest.receiptPictures || [], submitter); + try { + await sendPendingSaboSubmissionNotification( + updatedReimbursementRequest.notificationSlackThreads, + submitter.userId, + updatedReimbursementRequest.recipientId, + updatedReimbursementRequest.reimbursementRequestId + ); + } catch (e: unknown) { + console.error('Error sending pending SABO submission notification:', e); + } + return updatedReimbursementRequest; } @@ -611,11 +591,7 @@ export default class ReimbursementRequestService { * @param organizationId the organization the user is currently in * @returns reimbursement requests with no advisor approved reimbursement status */ - static async getPendingAdvisorList( - requester: User, - organization: Organization, - carNumber?: number - ): Promise { + static async getPendingAdvisorList(requester: User, organization: Organization): Promise { await validateUserIsPartOfFinanceTeamOrHead(requester, organization.organizationId); const requestsPendingAdvisors = await prisma.reimbursement_Request.findMany({ @@ -629,13 +605,7 @@ export default class ReimbursementRequestService { type: Reimbursement_Status_Type.ADVISOR_APPROVED } }, - accountCode: { organizationId: organization.organizationId }, - ...(carNumber !== undefined && - carNumber !== null && { - reimbursementProducts: { - some: { reimbursementProductReason: { wbsElement: { carNumber } } } - } - }) + accountCode: { organizationId: organization.organizationId } }, ...getReimbursementRequestQueryArgs(organization.organizationId) }); @@ -1022,26 +992,13 @@ export default class ReimbursementRequestService { * @param organizationId the organization the user is currently in * @returns an array of the prisma version of the reimbursement requests transformed to the shared version */ - static async getAllReimbursementRequests( - user: User, - organization: Organization, - carNumber?: number - ): Promise { + static async getAllReimbursementRequests(user: User, organization: Organization): Promise { if (!(await isUserFinanceTeamOrHead(user, organization.organizationId))) { throw new AccessDeniedException(`You are not a member of the finance team!`); } const reimbursementRequests = await prisma.reimbursement_Request.findMany({ - where: { - dateDeleted: null, - accountCode: { organizationId: organization.organizationId }, - ...(carNumber !== undefined && - carNumber !== null && { - reimbursementProducts: { - some: { reimbursementProductReason: { wbsElement: { carNumber } } } - } - }) - }, + where: { dateDeleted: null, accountCode: { organizationId: organization.organizationId } }, ...getReimbursementRequestQueryArgs(organization.organizationId) }); diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 69bf4cf9e5..dd6c40f929 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -161,11 +161,7 @@ export default class UsersService { * @param organizationId the id of the organization the user is in * @returns the user's favorite projects */ - static async getUsersFavoriteProjects( - userId: string, - organization: Organization, - carId?: string - ): Promise { + static async getUsersFavoriteProjects(userId: string, organization: Organization): Promise { const requestedUser = await prisma.user.findUnique({ where: { userId } }); if (!requestedUser) throw new NotFoundException('User', userId); @@ -179,8 +175,7 @@ export default class UsersService { wbsElement: { organizationId: organization.organizationId, dateDeleted: null - }, - ...(carId && { carId }) + } }, ...getProjectOverviewQueryArgs(organization.organizationId) }); diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 83feb938d8..5db4b1a99f 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -46,26 +46,21 @@ export default class WorkPackagesService { * * @param query the filters on the query * @param organizationId the id of the organization that the user is currently in - * @param carId the car number to filter by (only returns work packages from this car when provided) * @returns a list of work packages */ static async getAllWorkPackages( query: { - status?: WbsElementStatus | string; + status?: WbsElementStatus; daysUntilDeadline?: string; }, - organization: Organization, - carId?: string + organization: Organization ): Promise { const workPackages = await prisma.work_Package.findMany({ - where: { - wbsElement: { dateDeleted: null, organizationId: organization.organizationId }, - ...(carId && { project: { carId } }) - }, + where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId } }, ...getWorkPackageQueryArgs(organization.organizationId) }); - const filteredWorkPackages = workPackages.map(workPackageTransformer).filter((wp) => { + const outputWorkPackages = workPackages.map(workPackageTransformer).filter((wp) => { let passes = true; if (query.status) passes &&= wp.status === query.status; if (query.daysUntilDeadline) { @@ -75,9 +70,9 @@ export default class WorkPackagesService { return passes; }); - filteredWorkPackages.sort((wpA, wpB) => wpA.endDate.getTime() - wpB.endDate.getTime()); + outputWorkPackages.sort((wpA, wpB) => wpA.endDate.getTime() - wpB.endDate.getTime()); - return filteredWorkPackages; + return outputWorkPackages; } /** @@ -85,13 +80,11 @@ export default class WorkPackagesService { * * @param status Optional status filter * @param organization the organization - * @param carId the car number to filter by (only returns work packages from this car when provided) * @returns a list of work package previews */ static async getAllWorkPackagesPreview( status: WbsElementStatus | string | undefined, - organization: Organization, - carId?: string + organization: Organization ): Promise { const workPackages = await prisma.work_Package.findMany({ where: { @@ -99,8 +92,7 @@ export default class WorkPackagesService { dateDeleted: null, organizationId: organization.organizationId, ...(status ? { status: status as WbsElementStatus } : {}) - }, - ...(carId && { project: { carId } }) + } }, ...getWorkPackagePreviewQueryArgs() }); @@ -149,7 +141,6 @@ export default class WorkPackagesService { * Retrieve a subset of work packages. * @param wbsNums the WBS numbers of the work packages to retrieve * @param organizationId the id of the organization that the user is currently in - * @param carId optional car number to filter work packages by * @returns the work packages with the given WBS numbers * @throws if any of the work packages are not found or are not part of the organization */ @@ -163,24 +154,12 @@ export default class WorkPackagesService { } }); - const whereConditions = wbsNums.map((wbsNum) => ({ - wbsElement: { - carNumber: wbsNum.carNumber, - projectNumber: wbsNum.projectNumber, - workPackageNumber: wbsNum.workPackageNumber, - organizationId: organization.organizationId, - dateDeleted: null - } - })); - - const workPackages = await prisma.work_Package.findMany({ - where: { - OR: whereConditions - }, - ...getWorkPackageQueryArgs(organization.organizationId) + const workPackagePromises = wbsNums.map(async (wbsNum) => { + return WorkPackagesService.getSingleWorkPackage(wbsNum, organization); }); - return workPackages.map(workPackageTransformer); + const resolvedWorkPackages = await Promise.all(workPackagePromises); + return resolvedWorkPackages; } /** @@ -539,7 +518,6 @@ export default class WorkPackagesService { * Gets the work packages the given work package is blocking * @param wbsNum the wbs number of the work package to get the blocking work packages for * @param organizationId the id of the organization that the user is currently in - * @param carId the optional carId to filter work packages by * @returns the blocking work packages for the given work package */ static async getBlockingWorkPackages(wbsNum: WbsNumber, organization: Organization): Promise { @@ -570,6 +548,7 @@ export default class WorkPackagesService { throw new InvalidOrganizationException('Work Package'); const blockingWorkPackages = await getBlockingWorkPackages(workPackage); + return blockingWorkPackages.map(workPackageTransformer); } @@ -614,14 +593,12 @@ export default class WorkPackagesService { * * @param user The current user * @param organization The organization the current user is logged in for - * @param selection The selection type for filtering workpackages - * @param carId Optional car number to filter work packages by + * @param onlyOverdue Whether to only return overdue workpackages */ static async getHomePageWorkPackages( user: User, organization: Organization, - selection: WorkPackageSelection, - carId?: string + selection: WorkPackageSelection ): Promise { const selectionArgs = selection === WorkPackageSelection.ALL_OVERDUE @@ -657,8 +634,7 @@ export default class WorkPackagesService { dateDeleted: null, organizationId: organization.organizationId, status: { not: WBS_Element_Status.COMPLETE } - }, - ...(carId && { project: { carId } }) + } }, select: { project: { select: { projectId: true, wbsElement: { select: { name: true } } } }, diff --git a/src/backend/src/transformers/attendance.transformer.ts b/src/backend/src/transformers/attendance.transformer.ts deleted file mode 100644 index c9ca5edbcc..0000000000 --- a/src/backend/src/transformers/attendance.transformer.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { MeetingAttendance, MeetingAttendanceWithAttendees } from 'shared'; -import { - MeetingAttendanceQueryArgs, - MeetingAttendanceWithAttendeesQueryArgs -} from '../prisma-query-args/attendance.query-args.js'; -import { userTransformer } from './user.transformer.js'; - -export const meetingAttendanceTransformer = ( - attendance: Prisma.Meeting_AttendanceGetPayload -): MeetingAttendance => { - const teamMemberIds = new Set([ - ...attendance.team.members.map((m) => m.userId), - ...attendance.team.leads.map((l) => l.userId), - attendance.team.headId - ]); - const attendeeIds = new Set(attendance.attendees.map((a) => a.userId)); - const teamMemberAttendees = [...teamMemberIds].filter((id) => attendeeIds.has(id)).length; - - return { - meetingAttendanceId: attendance.meetingAttendanceId, - teamId: attendance.teamId, - teamName: attendance.team.teamName, - userCreated: userTransformer(attendance.userCreated), - openedAt: attendance.openedAt, - closedAt: attendance.closedAt ?? undefined, - attendeesCount: attendance.attendees.length, - teamMemberAttendancePercent: teamMemberIds.size > 0 ? (teamMemberAttendees / teamMemberIds.size) * 100 : 0 - }; -}; - -export const meetingAttendanceWithAttendeesTransformer = ( - attendance: Prisma.Meeting_AttendanceGetPayload -): MeetingAttendanceWithAttendees => { - const teamMemberIds = new Set([ - ...attendance.team.members.map((m) => m.userId), - ...attendance.team.leads.map((l) => l.userId), - attendance.team.headId - ]); - const attendeeIds = new Set(attendance.attendees.map((a) => a.userId)); - const teamMemberAttendees = [...teamMemberIds].filter((id) => attendeeIds.has(id)).length; - - return { - meetingAttendanceId: attendance.meetingAttendanceId, - teamId: attendance.teamId, - teamName: attendance.team.teamName, - userCreated: userTransformer(attendance.userCreated), - openedAt: attendance.openedAt, - closedAt: attendance.closedAt ?? undefined, - attendeesCount: attendance.attendees.length, - teamMemberAttendancePercent: teamMemberIds.size > 0 ? (teamMemberAttendees / teamMemberIds.size) * 100 : 0, - attendees: attendance.attendees.map(userTransformer) - }; -}; diff --git a/src/backend/src/transformers/change-requests.transformer.ts b/src/backend/src/transformers/change-requests.transformer.ts index 63f6723c88..1c39c2f040 100644 --- a/src/backend/src/transformers/change-requests.transformer.ts +++ b/src/backend/src/transformers/change-requests.transformer.ts @@ -10,8 +10,7 @@ import { WorkPackageStage, BudgetChangeRequest, isWorkPackageWbs, - LeadershipChangeRequest, - ChangeRequestStatus + LeadershipChangeRequest } from 'shared'; import { wbsNumOf } from '../utils/utils.js'; import { calculateChangeRequestStatus, convertCRScopeWhyType } from '../utils/change-requests.utils.js'; @@ -26,12 +25,10 @@ import { } from '../prisma-query-args/scope-change-requests.query-args.js'; import { HttpException } from '../utils/errors.utils.js'; import { - ChangeRequestGuestQueryArgs, ChangeRequestManyQueryArgs, ChangeRequestWithProjectAndWorkPackageQueryArgs } from '../prisma-query-args/change-requests.query-args.js'; import { accountCodeTransformer, otherProductReasonTransformer } from './reimbursement-requests.transformer.js'; -import { GuestChangeRequest } from '../../../shared/src/types/change-request-types.js'; const projectProposedChangesTransformer = ( wbsProposedChanges: Prisma.Wbs_Proposed_ChangesGetPayload @@ -232,41 +229,3 @@ const changeRequestTransformer = ( }; export default changeRequestTransformer; - -export const guestChangeRequestTransformer = ( - changeRequest: Prisma.Change_RequestGetPayload -): GuestChangeRequest => { - const status = changeRequest.changes.length - ? ChangeRequestStatus.Implemented - : changeRequest.accepted && changeRequest.dateReviewed - ? ChangeRequestStatus.Accepted - : changeRequest.dateReviewed - ? ChangeRequestStatus.Denied - : ChangeRequestStatus.Open; - - const wbsName = changeRequest.wbsElement - ? !isWorkPackageWbs(changeRequest.wbsElement) - ? changeRequest.wbsElement?.name - : `${changeRequest.wbsElement?.workPackage?.project.wbsElement.name} - ${changeRequest.wbsElement?.name}` - : undefined; - - return { - crId: changeRequest.crId, - submitter: userTransformer(changeRequest.submitter), - identifier: changeRequest.identifier, - type: changeRequest.type, - status, - teamTypeNames: changeRequest.wbsElement - ? isWorkPackageWbs(changeRequest.wbsElement) - ? (changeRequest.wbsElement.workPackage?.project?.teams - .map((team) => team.teamType?.name) - .filter((name) => name !== undefined) ?? []) - : (changeRequest.wbsElement.project?.teams.map((team) => team.teamType?.name).filter((name) => name !== undefined) ?? - []) - : [], - accepted: changeRequest.accepted ?? undefined, - reviewer: changeRequest.reviewer ? userTransformer(changeRequest.reviewer) : undefined, - wbsNum: changeRequest.wbsElement ? wbsNumOf(changeRequest.wbsElement) : undefined, - wbsName - }; -}; diff --git a/src/backend/src/transformers/faq.transformer.ts b/src/backend/src/transformers/faq.transformer.ts new file mode 100644 index 0000000000..6d3e6a81a3 --- /dev/null +++ b/src/backend/src/transformers/faq.transformer.ts @@ -0,0 +1,13 @@ +import { Prisma } from '@prisma/client'; +import { FrequentlyAskedQuestion } from 'shared'; +import { FaqQueryArgs } from '../prisma-query-args/faq.query-args.js'; +import { userTransformer } from './user.transformer.js'; + +export const faqTransformer = (faq: Prisma.FrequentlyAskedQuestionGetPayload): FrequentlyAskedQuestion => ({ + faqId: faq.faqId, + question: faq.question, + answer: faq.answer, + userCreated: userTransformer(faq.userCreated), + dateCreated: faq.dateCreated, + dateDeleted: faq.dateDeleted ?? undefined +}); diff --git a/src/backend/src/transformers/material.transformer.ts b/src/backend/src/transformers/material.transformer.ts index 58967b542d..10e049e15e 100644 --- a/src/backend/src/transformers/material.transformer.ts +++ b/src/backend/src/transformers/material.transformer.ts @@ -48,8 +48,7 @@ export const materialTransformer = (material: Prisma.MaterialGetPayload p.reimbursementRequest && !p.reimbursementRequest.dateDeleted) .map((p) => [p.reimbursementRequest!.reimbursementRequestId, p.reimbursementRequest!]) ).values() - ), - isCopied: material.isCopied + ) }; }; diff --git a/src/backend/src/transformers/projects.transformer.ts b/src/backend/src/transformers/projects.transformer.ts index e4ba1e146f..85325ae54f 100644 --- a/src/backend/src/transformers/projects.transformer.ts +++ b/src/backend/src/transformers/projects.transformer.ts @@ -117,19 +117,6 @@ export const projectPreviewTransformer = (project: Prisma.ProjectGetPayload { - if (team.teamType) { - acc.set(team.teamType.teamTypeId, { - name: team.teamType.name, - teamTypeId: team.teamType.teamTypeId - }); - } - return acc; - }, new Map()) - .values() - ), teams: project.teams, workPackages: project.workPackages.map((wp) => ({ ...wp, @@ -154,7 +141,7 @@ export const projectPreviewTransformer = (project: Prisma.ProjectGetPayload): ProjectOverview => { return { ...projectPreviewTransformer(project), - tasksRemaining: project.wbsElement._count.tasks, + tasks: project.wbsElement.tasks.map(taskTransformer), links: project.wbsElement.links }; }; diff --git a/src/backend/src/transformers/recruitment-transformer.ts b/src/backend/src/transformers/recruitment-transformer.ts deleted file mode 100644 index 1fbbb87b35..0000000000 --- a/src/backend/src/transformers/recruitment-transformer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { FrequentlyAskedQuestion, GuestDefinition } from 'shared'; -import { FaqQueryArgs } from '../prisma-query-args/faq.query-args.js'; -import { userTransformer } from './user.transformer.js'; - -export const faqTransformer = (faq: Prisma.FrequentlyAskedQuestionGetPayload): FrequentlyAskedQuestion => ({ - faqId: faq.faqId, - question: faq.question, - answer: faq.answer, - userCreated: userTransformer(faq.userCreated), - dateCreated: faq.dateCreated, - dateDeleted: faq.dateDeleted ?? undefined -}); - -export const guestDefinitionTransformer = (guestDefinition: Prisma.Guest_DefinitionGetPayload<{}>): GuestDefinition => ({ - definitionId: guestDefinition.definitionId, - term: guestDefinition.term, - description: guestDefinition.description, - order: guestDefinition.order, - buttonText: guestDefinition.buttonText ?? undefined, - buttonLink: guestDefinition.buttonLink ?? undefined, - icon: guestDefinition.icon ?? undefined -}); diff --git a/src/backend/src/transformers/teams.transformer.ts b/src/backend/src/transformers/teams.transformer.ts index 0600e2ca72..f49d9edd46 100644 --- a/src/backend/src/transformers/teams.transformer.ts +++ b/src/backend/src/transformers/teams.transformer.ts @@ -3,7 +3,6 @@ import { Team, TeamPreview, TeamBase } from 'shared'; import { getTeamBaseQueryArgs, TeamPreviewQueryArgs, TeamQueryArgs } from '../prisma-query-args/teams.query-args.js'; import { userTransformer } from './user.transformer.js'; import { projectGanttTransformer } from './projects.transformer.js'; -import { teamTypeTransformer } from './team-types.transformer.js'; const teamTransformer = (team: Prisma.TeamGetPayload): Team => { return { @@ -39,7 +38,7 @@ export const teamPreviewTransformer = (team: Prisma.TeamGetPayload { - return jwt.sign(user, TOKEN_SECRET, { expiresIn: '7d' }); + return jwt.sign(user, TOKEN_SECRET, { expiresIn: '12h' }); }; // headers needed for production @@ -24,8 +24,7 @@ export const prodHeaders = [ 'XMLHttpRequest', 'X-Auth-Token', 'Client-Security-Token', - 'organizationId', - 'carId' + 'organizationId' ]; // middleware function for production that will enforce jwt authorization diff --git a/src/backend/src/utils/car.utils.ts b/src/backend/src/utils/car.utils.ts deleted file mode 100644 index 9e645280dd..0000000000 --- a/src/backend/src/utils/car.utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import prisma from '../prisma/prisma.js'; -import { NotFoundException } from './errors.utils.js'; - -export const getCurrentCar = async (req: Request, _res: Response, next: NextFunction) => { - const carId = req.headers.carid; - - if (!carId || typeof carId !== 'string') { - return next(); - } - - try { - const car = await prisma.car.findUnique({ - where: { - carId, - wbsElement: { organizationId: req.organization.organizationId } - }, - include: { wbsElement: true } - }); - - if (!car) { - throw new NotFoundException('Car', carId); - } - - req.currentCar = car; - return next(); - } catch (error) { - return next(error); - } -}; diff --git a/src/backend/src/utils/errors.utils.ts b/src/backend/src/utils/errors.utils.ts index c9886b4781..8ef8e17166 100644 --- a/src/backend/src/utils/errors.utils.ts +++ b/src/backend/src/utils/errors.utils.ts @@ -211,7 +211,5 @@ export type ExceptionObjectNames = | 'Event Type' | 'Event' | 'Schedule Slot' - | 'Guest Definition' | 'ProspectiveSponsor' - | 'SponsorTier' - | 'Meeting Attendance'; + | 'SponsorTier'; diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index e59a638920..d5a111532c 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -1,4 +1,4 @@ -import { Task as Prisma_Task, WBS_Element, Event, Work_Package, Team, Event_Type } from '@prisma/client'; +import { Task as Prisma_Task, WBS_Element, Event, Work_Package } from '@prisma/client'; import { UserWithSettings } from './auth.utils.js'; import { ScheduleSlot } from 'shared'; @@ -10,8 +10,6 @@ export type TaskWithAssignees = Prisma_Task & { export type EventWithAttendees = Event & { attendees: UserWithSettings[]; scheduledTimes: ScheduleSlot[]; - teams: Team[]; - eventType: Event_Type; workPackages: (Work_Package & { wbsElement: WBS_Element; })[]; @@ -31,10 +29,7 @@ export const userToSlackPing = (user: UserWithSettings) => { * @returns the beginning of the day tomorrow (at 12am) */ export const startOfDayTomorrow = () => { - const tomorrow = new Date(); - tomorrow.setUTCDate(tomorrow.getUTCDate() + 1); - tomorrow.setUTCHours(0, 0, 0, 0); - return tomorrow; + return new Date(new Date().setHours(24, 0, 0, 0)); }; /** @@ -44,36 +39,6 @@ export const startOfDayTomorrow = () => { export const endOfDayTomorrow = () => { const startOfDay = startOfDayTomorrow(); const endOfDay = new Date(startOfDay); - endOfDay.setUTCDate(startOfDay.getUTCDate() + 1); + endOfDay.setDate(startOfDay.getDate() + 1); return endOfDay; }; - -const EST_OFFSET_MS = 5 * 60 * 60 * 1000; - -/** - * Given a UTC Date, returns the start of that calendar day in EST (UTC-5), expressed as a UTC Date. - * EST is always treated as UTC-5 (no DST adjustment). - * @returns midnight EST of the given date as a UTC Date - */ -export const startOfDateEST = (date: Date): Date => { - const dateInEST = new Date(date.getTime() - EST_OFFSET_MS); - return new Date(Date.UTC(dateInEST.getUTCFullYear(), dateInEST.getUTCMonth(), dateInEST.getUTCDate(), 5, 0, 0, 0)); -}; - -/** - * Gets the start of today in EST (UTC-5), expressed as a UTC Date. - * EST is always treated as UTC-5 (no DST adjustment). - * @returns midnight EST today as a UTC Date - */ -export const startOfTodayEST = (): Date => startOfDateEST(new Date()); - -/** - * Gets the start of tomorrow in EST (UTC-5), expressed as a UTC Date. - * EST is always treated as UTC-5 (no DST adjustment). - * @returns midnight EST tomorrow as a UTC Date - */ -export const startOfTomorrowEST = (): Date => { - const start = startOfTodayEST(); - start.setUTCDate(start.getUTCDate() + 1); - return start; -}; diff --git a/src/backend/src/utils/slack.utils.ts b/src/backend/src/utils/slack.utils.ts index b85a4ca721..878086d811 100644 --- a/src/backend/src/utils/slack.utils.ts +++ b/src/backend/src/utils/slack.utils.ts @@ -127,37 +127,18 @@ export const sendSlackTaskAssignedNotification = async ( /** * Send a notification to users that a reimbursement request is created on Slack - * @param requestId the id of the reimbursement request + * @param requestId the id if the reimbursement request * @param submitterId the id of the user who created the reimbursement request - * @param organizationId the organization id of the current user */ export const sendReimbursementRequestCreatedNotificationAndCreateMessageInfo = async ( requestId: string, + requestIdentifier: number, submitterId: string, organizationId: string ): Promise => { if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return; // don't send msgs unless in prod - const reimbursementRequest = await prisma.reimbursement_Request.findUnique({ - where: { reimbursementRequestId: requestId }, - select: { - identifier: true, - totalCost: true, - description: true, - vendor: { - select: { - name: true - } - } - } - }); - - if (!reimbursementRequest) throw new HttpException(500, 'Reimbursement request does not exist!'); - - const { identifier, totalCost, description, vendor } = reimbursementRequest; - const formattedCost = `$${(totalCost / 100).toFixed(2)}`; // convert from cents to dollars and cents - - const msg = `${await getUserSlackMentionOrName(submitterId)} created a reimbursement request for ${formattedCost} at ${vendor.name} (ID#: ${identifier}) 💲`; + const msg = `${await getUserSlackMentionOrName(submitterId)} created a reimbursement request (ID#: ${requestIdentifier}) 💲`; const link = `https://finishlinebyner.com/finance/reimbursement-requests/${requestId}`; const linkButtonText = 'View Reimbursement Request'; @@ -170,23 +151,13 @@ export const sendReimbursementRequestCreatedNotificationAndCreateMessageInfo = a const messageInfo = await sendMessage(financeTeam.slackId, msg, link, linkButtonText); if (!messageInfo) return; - const createdMessageInfo = await prisma.message_Info.create({ + await prisma.message_Info.create({ data: { reimbursementRequestId: requestId, channelId: messageInfo.channelId, timestamp: messageInfo.ts } }); - - const { messageInfoId, channelId, timestamp } = createdMessageInfo; - - // send reimbursement request description in slack thread - if (description) { - await sendThreadResponse( - [{ messageInfoId, channelId, timestamp, changeRequestId: null }], - `Description: ${description}` - ); - } }; /** diff --git a/src/backend/src/utils/validation.utils.ts b/src/backend/src/utils/validation.utils.ts index 5e5a8758d5..153b32a0bf 100644 --- a/src/backend/src/utils/validation.utils.ts +++ b/src/backend/src/utils/validation.utils.ts @@ -317,7 +317,8 @@ export const partPopupValidators = [ export const financeDashboardFilterValidators = [ nonEmptyString(query('startDate')).optional(), - nonEmptyString(query('endDate')).optional() + nonEmptyString(query('endDate')).optional(), + nonEmptyString(query('carNumber')).optional() ]; export const requireFile = (chain: ValidationChain): ValidationChain => { diff --git a/src/backend/src/utils/work-packages.utils.ts b/src/backend/src/utils/work-packages.utils.ts index ab4c231273..4e17ebd75b 100644 --- a/src/backend/src/utils/work-packages.utils.ts +++ b/src/backend/src/utils/work-packages.utils.ts @@ -37,7 +37,7 @@ export const getBlockingWorkPackages = async (initialWorkPackage: Prisma.Work_Pa where: { wbsElementId: currWbsId }, include: { blocking: true, - workPackage: getWorkPackageQueryArgs(initialWorkPackage.wbsElement.organizationId) + workPackage: { ...getWorkPackageQueryArgs(initialWorkPackage.wbsElement.organizationId) } } }); diff --git a/src/backend/tests/test-utils.ts b/src/backend/tests/test-utils.ts index f46df77c0a..e540886f08 100644 --- a/src/backend/tests/test-utils.ts +++ b/src/backend/tests/test-utils.ts @@ -122,7 +122,6 @@ export const resetUsers = async () => { await prisma.manufacturer.deleteMany(); await prisma.material_Type.deleteMany(); await prisma.assembly.deleteMany(); - await prisma.meeting_Attendance.deleteMany(); await prisma.team.deleteMany(); await prisma.user_Secure_Settings.deleteMany(); await prisma.receipt.deleteMany(); @@ -178,8 +177,6 @@ export const resetUsers = async () => { await prisma.shop_Machinery.deleteMany(); await prisma.machinery.deleteMany(); await prisma.shop.deleteMany(); - await prisma.description_Bullet.deleteMany(); - await prisma.description_Bullet_Type.deleteMany(); await prisma.organization.deleteMany(); await prisma.user.deleteMany(); }; @@ -397,7 +394,7 @@ export const createTestLinkType = async (user: User, organizationId?: string) => return linkType; }; -export const createTestCar = async (orgId?: string, userIdentification?: string, carNumber: number = 0) => { +export const createTestCar = async (orgId?: string, userIdentification?: string) => { if (!orgId) orgId = (await createTestOrganization()).organizationId; if (!userIdentification) userIdentification = (await createTestUser(supermanAdmin, orgId)).userId; @@ -405,7 +402,7 @@ export const createTestCar = async (orgId?: string, userIdentification?: string, data: { wbsElement: { create: { - carNumber, + carNumber: 0, projectNumber: 0, workPackageNumber: 0, dateCreated: new Date('01/01/2023'), @@ -427,7 +424,6 @@ export const createTestProject = async ( organizationId?: string, teamId?: string, carId?: string, - carNumber: number = 0, projectNumber: number = 1, dateDeleted?: Date ): Promise => { @@ -438,7 +434,7 @@ export const createTestProject = async ( data: { wbsElement: { create: { - carNumber, + carNumber: 0, projectNumber, workPackageNumber: 0, dateCreated: new Date('01/01/2023'), @@ -478,36 +474,6 @@ export const createTestProject = async ( return genesisProject; }; -export const createTestWorkPackage = async ( - user: User, - organizationId: string, - projectId: string, - carNumber: number = 0, - projectNumber: number = 1, - workPackageNumber: number = 1 -) => - prisma.work_Package.create({ - data: { - wbsElement: { - create: { - carNumber, - projectNumber, - workPackageNumber, - name: `WP ${carNumber}.${projectNumber}.${workPackageNumber}`, - status: WBS_Element_Status.ACTIVE, - leadId: user.userId, - managerId: user.userId, - organizationId - } - }, - project: { connect: { projectId } }, - startDate: new Date('2024-01-01'), - duration: 4, - orderInProject: workPackageNumber - }, - include: { wbsElement: true } - }); - export const createTestReimbursementRequest = async () => { const organization = await createTestOrganization(); await createFinanceTeamAndLead(organization); @@ -976,20 +942,3 @@ export const createMinimalPartReviewForReview = async ( return { review, partId: part.partId }; }; - -export const createTestGuestDefinition = async (user: User, organizationId: string) => { - if (!organizationId) organizationId = await createTestOrganization().then((org) => org.organizationId); - if (!organizationId) throw new Error('Failed to create organization'); - - const def = await prisma.guest_Definition.create({ - data: { - term: 'Term', - description: 'Description', - order: 0, - organizationId, - userCreatedId: user.userId - } - }); - - return def; -}; diff --git a/src/backend/tests/unit/attendance.test.ts b/src/backend/tests/unit/attendance.test.ts deleted file mode 100644 index 91a71896b0..0000000000 --- a/src/backend/tests/unit/attendance.test.ts +++ /dev/null @@ -1,567 +0,0 @@ -import { Organization } from '@prisma/client'; -import { vi } from 'vitest'; -import AttendanceService from '../../src/services/attendance.services.js'; -import { AccessDeniedException, HttpException, NotFoundException } from '../../src/utils/errors.utils.js'; -import { batmanAppAdmin, supermanAdmin, greenlanternHead, wonderwomanGuest, member } from '../test-data/users.test-data.js'; -import { createTestOrganization, createTestTeam, createTestTeamType, createTestUser, resetUsers } from '../test-utils.js'; -import prisma from '../../src/prisma/prisma.js'; -import { - sendMessage, - editMessage, - replyToMessageInThread, - getChannelName, - checkBotInChannel -} from '../../src/integrations/slack.js'; -import { Mock } from 'vitest'; - -vi.mock('../../src/integrations/slack.js', () => ({ - sendMessage: vi.fn(), - editMessage: vi.fn(), - replyToMessageInThread: vi.fn(), - getChannelName: vi.fn(), - checkBotInChannel: vi.fn() -})); - -// Creates a second test org with distinct credentials (since createTestOrganization uses a hardcoded empty email) -const createOtherOrganization = async () => { - const user = await prisma.user.create({ - data: { firstName: 'Other', lastName: 'Creator', email: 'other-org-creator@test.com', googleAuthId: 'otherOrgCreator' } - }); - return prisma.organization.create({ - data: { name: 'Other Org', description: '', applicationLink: '', userCreated: { connect: { userId: user.userId } } } - }); -}; - -// Creates a team with a team type properly scoped to orgId. -// (createTestTeam(head, undefined, orgId) incorrectly passes orgId as the team type name.) -const createTeamInOrg = async (headId: string, teamOrgId: string) => { - const teamType = await createTestTeamType('aTeam', teamOrgId); - return createTestTeam(headId, teamType.teamTypeId, teamOrgId); -}; - -describe('Attendance Tests', () => { - let organization: Organization; - let orgId: string; - - beforeEach(async () => { - organization = await createTestOrganization(); - orgId = organization.organizationId; - vi.clearAllMocks(); - }); - - afterEach(async () => { - await resetUsers(); - }); - - describe('takeAttendance', () => { - it('throws NotFoundException if team does not exist', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - await expect( - AttendanceService.takeAttendance(admin as any, 'nonexistent-team-id', 'Please react!', organization) - ).rejects.toThrow(new NotFoundException('Team', 'nonexistent-team-id')); - }); - - it('throws NotFoundException if team belongs to a different organization', async () => { - const otherOrg = await createOtherOrganization(); - const admin = await createTestUser(batmanAppAdmin, orgId); - const otherAdmin = await createTestUser({ ...supermanAdmin, googleAuthId: 'otherAdmin' }, otherOrg.organizationId); - const team = await createTeamInOrg(otherAdmin.userId, otherOrg.organizationId); - - await expect( - AttendanceService.takeAttendance(admin as any, team.teamId, 'Please react!', organization) - ).rejects.toThrow(new NotFoundException('Team', team.teamId)); - }); - - it('throws AccessDeniedException if user is not team head or admin', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const regularMember = await createTestUser({ ...member, googleAuthId: 'regularMember' }, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await expect( - AttendanceService.takeAttendance(regularMember as any, team.teamId, 'Please react!', organization) - ).rejects.toThrow(new AccessDeniedException('Only team heads or admins can take attendance')); - }); - - it('throws HttpException if there is already an open attendance session', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '1234567890.000001' - } - }); - - await expect( - AttendanceService.takeAttendance(head as any, team.teamId, 'Please react!', organization) - ).rejects.toThrow(new HttpException(400, 'There is already an open attendance session for this team')); - }); - - it('throws HttpException if Slack message fails to send', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (sendMessage as Mock).mockResolvedValue(null); - - await expect( - AttendanceService.takeAttendance(head as any, team.teamId, 'Please react!', organization) - ).rejects.toThrow( - new HttpException( - 500, - 'Failed to send Slack message. Check that the team Slack ID is valid and the bot is in the channel.' - ) - ); - }); - - it('succeeds and creates an attendance record when called by team head', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (sendMessage as Mock).mockResolvedValue({ channelId: 'C12345', ts: '1234567890.000001' }); - - const result = await AttendanceService.takeAttendance(head as any, team.teamId, 'Please react!', organization); - - expect(result.teamId).toBe(team.teamId); - expect(result.closedAt).toBeUndefined(); - expect(result.attendeesCount).toBe(0); - - const record = await prisma.meeting_Attendance.findFirst({ where: { teamId: team.teamId } }); - expect(record).not.toBeNull(); - expect(record!.slackChannelId).toBe('C12345'); - expect(record!.slackMessageTimestamp).toBe('1234567890.000001'); - }); - - it('succeeds and creates an attendance record when called by admin', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const admin = await createTestUser(batmanAppAdmin, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (sendMessage as Mock).mockResolvedValue({ channelId: 'C99999', ts: '9999999999.000001' }); - - const result = await AttendanceService.takeAttendance(admin as any, team.teamId, 'Attend please!', organization); - - expect(result.teamId).toBe(team.teamId); - expect(result.userCreated.userId).toBe(admin.userId); - }); - }); - - describe('getAllAttendances', () => { - it('returns empty array when no attendances exist', async () => { - const result = await AttendanceService.getAllAttendances(organization); - expect(result).toEqual([]); - }); - - it('returns all attendance records for the organization', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '222.002', - closedAt: new Date() - } - }); - - const result = await AttendanceService.getAllAttendances(organization); - expect(result).toHaveLength(2); - }); - - it('does not return attendance records from other organizations', async () => { - const otherOrg = await createOtherOrganization(); - const head = await createTestUser({ ...greenlanternHead, googleAuthId: 'otherHead' }, otherOrg.organizationId); - const team = await createTeamInOrg(head.userId, otherOrg.organizationId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: otherOrg.organizationId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - const result = await AttendanceService.getAllAttendances(organization); - expect(result).toHaveLength(0); - }); - }); - - describe('getOngoingAttendance', () => { - it('throws NotFoundException if team does not exist', async () => { - await expect(AttendanceService.getOngoingAttendance('nonexistent-team-id', organization)).rejects.toThrow( - new NotFoundException('Team', 'nonexistent-team-id') - ); - }); - - it('throws NotFoundException if team belongs to different organization', async () => { - const otherOrg = await createOtherOrganization(); - const head = await createTestUser({ ...greenlanternHead, googleAuthId: 'otherHead' }, otherOrg.organizationId); - const team = await createTeamInOrg(head.userId, otherOrg.organizationId); - - await expect(AttendanceService.getOngoingAttendance(team.teamId, organization)).rejects.toThrow( - new NotFoundException('Team', team.teamId) - ); - }); - - it('returns null if there is no ongoing attendance', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - const result = await AttendanceService.getOngoingAttendance(team.teamId, organization); - expect(result).toBeNull(); - }); - - it('returns null if the only attendance session is already closed', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001', - closedAt: new Date() - } - }); - - const result = await AttendanceService.getOngoingAttendance(team.teamId, organization); - expect(result).toBeNull(); - }); - - it('returns the ongoing attendance session', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - const result = await AttendanceService.getOngoingAttendance(team.teamId, organization); - expect(result).not.toBeNull(); - expect(result!.teamId).toBe(team.teamId); - expect(result!.closedAt).toBeUndefined(); - }); - }); - - describe('closeOngoingAttendance', () => { - it('throws NotFoundException if team does not exist', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - await expect( - AttendanceService.closeOngoingAttendance('nonexistent-team-id', admin as any, organization) - ).rejects.toThrow(new NotFoundException('Team', 'nonexistent-team-id')); - }); - - it('throws NotFoundException if team belongs to a different organization', async () => { - const otherOrg = await createOtherOrganization(); - const head = await createTestUser({ ...greenlanternHead, googleAuthId: 'otherHead' }, otherOrg.organizationId); - const team = await createTeamInOrg(head.userId, otherOrg.organizationId); - const admin = await createTestUser(batmanAppAdmin, orgId); - - await expect(AttendanceService.closeOngoingAttendance(team.teamId, admin as any, organization)).rejects.toThrow( - new NotFoundException('Team', team.teamId) - ); - }); - - it('throws AccessDeniedException if user is not team head or admin', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const regularMember = await createTestUser({ ...member, googleAuthId: 'regularMember' }, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - await expect( - AttendanceService.closeOngoingAttendance(team.teamId, regularMember as any, organization) - ).rejects.toThrow(new AccessDeniedException('Only team heads or admins can close attendance')); - }); - - it('throws HttpException if there is no open attendance session', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await expect(AttendanceService.closeOngoingAttendance(team.teamId, head as any, organization)).rejects.toThrow( - new HttpException(400, 'There is no open attendance session for this team') - ); - }); - - it('succeeds and closes the ongoing attendance when called by team head', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - (editMessage as Mock).mockResolvedValue(undefined); - - await AttendanceService.closeOngoingAttendance(team.teamId, head as any, organization); - - const record = await prisma.meeting_Attendance.findFirst({ where: { teamId: team.teamId } }); - expect(record!.closedAt).not.toBeNull(); - }); - - it('succeeds and closes the ongoing attendance when called by admin', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const admin = await createTestUser(batmanAppAdmin, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - (editMessage as Mock).mockResolvedValue(undefined); - - await AttendanceService.closeOngoingAttendance(team.teamId, admin as any, organization); - - const record = await prisma.meeting_Attendance.findFirst({ where: { teamId: team.teamId } }); - expect(record!.closedAt).not.toBeNull(); - }); - }); - - describe('closeAttendance', () => { - it('returns early if attendance does not exist', async () => { - await expect(AttendanceService.closeAttendance('nonexistent-id')).resolves.toBeUndefined(); - expect(editMessage).not.toHaveBeenCalled(); - }); - - it('returns early if attendance is already closed', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - const attendance = await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001', - closedAt: new Date() - } - }); - - await AttendanceService.closeAttendance(attendance.meetingAttendanceId); - expect(editMessage).not.toHaveBeenCalled(); - }); - - it('closes the attendance and edits the Slack message with summary', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const attendee = await createTestUser({ ...wonderwomanGuest, googleAuthId: 'attendeeUser' }, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.team.update({ - where: { teamId: team.teamId }, - data: { members: { connect: { userId: attendee.userId } } } - }); - - const attendance = await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001', - attendees: { connect: { userId: attendee.userId } } - } - }); - - (editMessage as Mock).mockResolvedValue(undefined); - - await AttendanceService.closeAttendance(attendance.meetingAttendanceId); - - const record = await prisma.meeting_Attendance.findUnique({ - where: { meetingAttendanceId: attendance.meetingAttendanceId } - }); - expect(record!.closedAt).not.toBeNull(); - expect(editMessage).toHaveBeenCalledWith('C12345', '111.001', expect.stringContaining('Attendance is now closed.')); - }); - - it('calculates correct attendance percentage', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const member1 = await createTestUser({ ...member, googleAuthId: 'member1' }, orgId); - const member2 = await createTestUser({ ...wonderwomanGuest, googleAuthId: 'member2' }, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.team.update({ - where: { teamId: team.teamId }, - data: { members: { connect: [{ userId: member1.userId }, { userId: member2.userId }] } } - }); - - // Only member1 and head attended (2 out of 3 team members) - const attendance = await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001', - attendees: { connect: [{ userId: head.userId }, { userId: member1.userId }] } - } - }); - - (editMessage as Mock).mockResolvedValue(undefined); - - await AttendanceService.closeAttendance(attendance.meetingAttendanceId); - - // 2 attendees / 3 team members = 66.7% - expect(editMessage).toHaveBeenCalledWith('C12345', '111.001', expect.stringContaining('66.7% of team')); - }); - }); - - describe('handleReactionAdded', () => { - it('returns early if no matching open attendance exists', async () => { - await AttendanceService.handleReactionAdded('slackUser1', 'C99999', 'nonexistent.ts'); - expect(replyToMessageInThread).not.toHaveBeenCalled(); - }); - - it('posts a thread message if the Slack user is not linked to a FinishLine account', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - (replyToMessageInThread as Mock).mockResolvedValue(undefined); - - await AttendanceService.handleReactionAdded('unknownSlackId', 'C12345', '111.001'); - - expect(replyToMessageInThread).toHaveBeenCalledWith('C12345', '111.001', expect.stringContaining('unknownSlackId')); - }); - - it('records attendance when a linked user reacts', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const attendee = await createTestUser({ ...wonderwomanGuest, googleAuthId: 'attendeeUser' }, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - await prisma.user_Settings.create({ - data: { userId: attendee.userId, slackId: 'slackWW123' } - }); - - const attendance = await prisma.meeting_Attendance.create({ - data: { - organizationId: orgId, - teamId: team.teamId, - userCreatedId: head.userId, - slackChannelId: 'C12345', - slackMessageTimestamp: '111.001' - } - }); - - await AttendanceService.handleReactionAdded('slackWW123', 'C12345', '111.001'); - - const updated = await prisma.meeting_Attendance.findUnique({ - where: { meetingAttendanceId: attendance.meetingAttendanceId }, - include: { attendees: true } - }); - expect(updated!.attendees).toHaveLength(1); - expect(updated!.attendees[0].userId).toBe(attendee.userId); - expect(replyToMessageInThread).not.toHaveBeenCalled(); - }); - }); - - describe('checkTeamChannel', () => { - it('throws NotFoundException if team does not exist', async () => { - await expect(AttendanceService.checkTeamChannel('nonexistent-team-id', organization)).rejects.toThrow( - new NotFoundException('Team', 'nonexistent-team-id') - ); - }); - - it('throws NotFoundException if team belongs to a different organization', async () => { - const otherOrg = await createOtherOrganization(); - const head = await createTestUser({ ...greenlanternHead, googleAuthId: 'otherHead' }, otherOrg.organizationId); - const team = await createTeamInOrg(head.userId, otherOrg.organizationId); - - await expect(AttendanceService.checkTeamChannel(team.teamId, organization)).rejects.toThrow( - new NotFoundException('Team', team.teamId) - ); - }); - - it('returns valid: false if Slack channel is not found', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (getChannelName as Mock).mockResolvedValue(undefined); - - const result = await AttendanceService.checkTeamChannel(team.teamId, organization); - - expect(result.valid).toBe(false); - expect(result.channelName).toBeUndefined(); - }); - - it('returns valid: false if Slack channel is found but bot is not a member', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (getChannelName as Mock).mockResolvedValue('ner-software'); - (checkBotInChannel as Mock).mockResolvedValue(false); - - const result = await AttendanceService.checkTeamChannel(team.teamId, organization); - - expect(result.valid).toBe(false); - expect(result.channelName).toBe('ner-software'); - }); - - it('returns valid: true with channel name if Slack channel is found and bot is a member', async () => { - const head = await createTestUser(greenlanternHead, orgId); - const team = await createTeamInOrg(head.userId, orgId); - - (getChannelName as Mock).mockResolvedValue('ner-software'); - (checkBotInChannel as Mock).mockResolvedValue(true); - - const result = await AttendanceService.checkTeamChannel(team.teamId, organization); - - expect(result.valid).toBe(true); - expect(result.channelName).toBe('ner-software'); - }); - }); -}); diff --git a/src/backend/tests/unit/car.utils.test.ts b/src/backend/tests/unit/car.utils.test.ts deleted file mode 100644 index c78f4a1344..0000000000 --- a/src/backend/tests/unit/car.utils.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/// -/// -import { Request, Response, NextFunction } from 'express'; -import { createTestCar, createTestOrganization, createTestUser, resetUsers } from '../test-utils.js'; -import { getCurrentCar } from '../../src/utils/car.utils.js'; -import { NotFoundException } from '../../src/utils/errors.utils.js'; -import { supermanAdmin } from '../test-data/users.test-data.js'; -import prisma from '../../src/prisma/prisma.js'; - -describe('getCurrentCar Middleware', () => { - let orgId: string; - - beforeEach(async () => { - const org = await createTestOrganization(); - orgId = org.organizationId; - }); - - afterEach(async () => { - await resetUsers(); - }); - - it('calls next() without setting req.currentCar when no carId header is present', async () => { - const req = { headers: {} } as unknown as Request; - const res = {} as Response; - const next = vi.fn() as unknown as NextFunction; - - await getCurrentCar(req, res, next); - - expect(next).toHaveBeenCalledWith(); - expect(req.currentCar).toBeUndefined(); - }); - - it('sets req.currentCar with wbsElement and calls next() when carId header matches an existing car', async () => { - const user = await createTestUser(supermanAdmin, orgId); - const car = await createTestCar(orgId, user.userId); - - const req = { headers: { carid: car.carId }, organization: { organizationId: orgId } } as unknown as Request; - const res = {} as Response; - const next = vi.fn() as unknown as NextFunction; - - await getCurrentCar(req, res, next); - - expect(next).toHaveBeenCalledWith(); - expect(req.currentCar).toBeDefined(); - expect(req.currentCar?.carId).toBe(car.carId); - expect(req.currentCar?.wbsElement).toBeDefined(); - }); - - it('calls next() with a NotFoundException when carId header does not match any car', async () => { - const req = { headers: { carid: 'non-existent-car-id' }, organization: { organizationId: orgId } } as unknown as Request; - const res = {} as Response; - const next = vi.fn() as unknown as NextFunction; - - await getCurrentCar(req, res, next); - - expect(next).toHaveBeenCalledWith(expect.any(NotFoundException)); - expect(req.currentCar).toBeUndefined(); - }); - - it('calls next() without error and does not set req.currentCar when carId header is an array', async () => { - const req = { headers: { carid: ['id-one', 'id-two'] } } as unknown as Request; - const res = {} as Response; - const next = vi.fn() as unknown as NextFunction; - - await getCurrentCar(req, res, next); - - expect(next).toHaveBeenCalledWith(); - expect(req.currentCar).toBeUndefined(); - }); - - it('calls next() with the thrown error when Prisma throws unexpectedly', async () => { - const dbError = new Error('DB connection lost'); - const spy = vi.spyOn(prisma.car, 'findUnique').mockRejectedValueOnce(dbError); - - const req = { headers: { carid: 'some-car-id' }, organization: { organizationId: orgId } } as unknown as Request; - const res = {} as Response; - const next = vi.fn() as unknown as NextFunction; - - await getCurrentCar(req, res, next); - - expect(next).toHaveBeenCalledWith(dbError); - - spy.mockRestore(); - }); -}); diff --git a/src/backend/tests/unit/change-requests.test.ts b/src/backend/tests/unit/change-requests.test.ts index cfdd23fecc..4b109c90f1 100644 --- a/src/backend/tests/unit/change-requests.test.ts +++ b/src/backend/tests/unit/change-requests.test.ts @@ -1,5 +1,5 @@ import { CR_Type, Organization, Scope_CR_Why_Type, User, WBS_Element_Status } from '@prisma/client'; -import { createTestCar, createTestOrganization, createTestProject, createTestUser, resetUsers } from '../test-utils.js'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils.js'; import ChangeRequestsService from '../../src/services/change-requests.services.js'; import { supermanAdmin, @@ -494,328 +494,4 @@ describe('Change Request Tests', () => { ).rejects.toThrow(AccessDeniedException); }); }); - - describe('global car filter', () => { - let carAId: string; - let carBId: string; - let otherUser: User; - - const solutionArgs = [{ description: 'Solution', scopeImpact: 'Low', timelineImpact: 0, budgetImpact: 0 }]; - - // projPropChanges makes a CR a scope CR - const projPropChanges = { - name: 'Updated project', - descriptionBullets: [], - links: [], - budget: 100, - summary: 'Summary', - teamIds: [], - workPackageProposedChanges: [] - }; - - beforeEach(async () => { - // The reviewing user (user) cannot be the submitter of scope CRs they review so otherUser is used . - otherUser = await createTestUser(aquamanLeadership, orgId); - - const carA = await createTestCar(orgId, user.userId, 0); - carAId = carA.carId; - const carB = await createTestCar(orgId, user.userId, 1); - carBId = carB.carId; - - // Project under car A: WBS 0.1.0, project.carId = carAId, lead/manager = user - await createTestProject(user, orgId, undefined, carAId, 0, 1); - // Project under car B: WBS 0.2.0, project.carId = carBId, lead/manager = user - await createTestProject(user, orgId, undefined, carBId, 0, 2); - }); - - describe('getAllChangeRequests', () => { - it('respects the global car filter and returns only CRs for the selected car', async () => { - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - const results = await ChangeRequestsService.getAllChangeRequests(organization, carAId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }); - - it('returns all CRs when no car is selected', async () => { - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - const results = await ChangeRequestsService.getAllChangeRequests(organization); - - expect(results).toHaveLength(2); - }); - }); - - describe('getToReviewChangeRequests', () => { - it('respects the global car filter and returns only to-review CRs for the selected car', async () => { - await ChangeRequestsService.createStandardChangeRequest( - otherUser, - 0, - 1, - 0, - CR_Type.DEFINITION_CHANGE, - 'Scope CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - [], - organization, - projPropChanges, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - otherUser, - 0, - 2, - 0, - CR_Type.DEFINITION_CHANGE, - 'Scope CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - [], - organization, - projPropChanges, - null - ); - - const results = await ChangeRequestsService.getToReviewChangeRequests(user, organization, carAId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }); - - it('returns all to-review CRs when no car is selected', async () => { - await ChangeRequestsService.createStandardChangeRequest( - otherUser, - 0, - 1, - 0, - CR_Type.DEFINITION_CHANGE, - 'Scope CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - [], - organization, - projPropChanges, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - otherUser, - 0, - 2, - 0, - CR_Type.DEFINITION_CHANGE, - 'Scope CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - [], - organization, - projPropChanges, - null - ); - - const results = await ChangeRequestsService.getToReviewChangeRequests(user, organization); - - expect(results).toHaveLength(2); - }); - }); - - describe('getUnreviewedChangeRequests', () => { - it('respects the global car filter and returns only unreviewed CRs for the selected car', async () => { - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'Unreviewed CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'Unreviewed CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - const results = await ChangeRequestsService.getUnreviewedChangeRequests(user, undefined, organization, carAId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }); - - it('ignores the global car filter when a wbsNum is provided and returns CRs matching the wbsNum', async () => { - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'Unreviewed CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'Unreviewed CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - // wbsNum scopes to car A's project; carId points to car B - car filter should be ignored - const wbsNum = { carNumber: 0, projectNumber: 1, workPackageNumber: 0 }; - const results = await ChangeRequestsService.getUnreviewedChangeRequests(user, wbsNum, organization, carBId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }); - }); - - describe('getApprovedChangeRequests', () => { - it('respects the global car filter and returns only recent CRs for the selected car', async () => { - const crA = await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'Recent CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - const crB = await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'Recent CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - // getApprovedChangeRequests requires dateReviewed >= fiveDaysAgo - review both CRs to satisfy this - await ChangeRequestsService.reviewChangeRequest(otherUser, crA.crId, '', false, organization, null); - await ChangeRequestsService.reviewChangeRequest(otherUser, crB.crId, '', false, organization, null); - - const results = await ChangeRequestsService.getApprovedChangeRequests(user, undefined, organization, carAId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }, 15000); - - it('ignores the global car filter when a wbsNum is provided and returns CRs matching the wbsNum', async () => { - const crA = await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 1, - 0, - CR_Type.ISSUE, - 'Recent CR on car A', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - const crB = await ChangeRequestsService.createStandardChangeRequest( - user, - 0, - 2, - 0, - CR_Type.ISSUE, - 'Recent CR on car B', - [{ type: Scope_CR_Why_Type.COMPETITION, explain: 'reason' }], - solutionArgs, - organization, - null, - null - ); - - // getApprovedChangeRequests requires dateReviewed >= fiveDaysAgo - review both CRs to satisfy this - await ChangeRequestsService.reviewChangeRequest(otherUser, crA.crId, '', false, organization, null); - await ChangeRequestsService.reviewChangeRequest(otherUser, crB.crId, '', false, organization, null); - - // wbsNum scopes to car A's project; carId points to car B - car filter should be ignored - const wbsNum = { carNumber: 0, projectNumber: 1, workPackageNumber: 0 }; - const results = await ChangeRequestsService.getApprovedChangeRequests(user, wbsNum, organization, carBId); - - expect(results).toHaveLength(1); - expect(results[0].wbsNum?.projectNumber).toBe(1); // car A's project - }, 15000); - }); - }); }); diff --git a/src/backend/tests/unit/part-review.test.ts b/src/backend/tests/unit/part-review.test.ts index f201c0e2c7..7ff46016f0 100644 --- a/src/backend/tests/unit/part-review.test.ts +++ b/src/backend/tests/unit/part-review.test.ts @@ -978,7 +978,7 @@ describe('part review tests', () => { const team = await createTestTeam(batman.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, batman.userId); - const project = await createTestProject(batman, orgId, team.teamId, car.carId, 0, 1); + const project = await createTestProject(batman, orgId, team.teamId, car.carId, 1); const project1 = await prisma.project.findUnique({ where: { projectId: project.projectId }, include: { @@ -1002,7 +1002,7 @@ describe('part review tests', () => { const team = await createTestTeam(batman.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, batman.userId); - const project = await createTestProject(batman, orgId, team.teamId, car.carId, 0, 1); + const project = await createTestProject(batman, orgId, team.teamId, car.carId, 1); const project1 = await prisma.project.findUnique({ where: { projectId: project.projectId }, include: { @@ -1024,7 +1024,7 @@ describe('part review tests', () => { const car = await createTestCar(orgId, batman.userId); // Create a project with no parts - await createTestProject(batman, orgId, team1.teamId, car.carId, 0, 4); + await createTestProject(batman, orgId, team1.teamId, car.carId, 4); const proj1WbsNum = validateWBS('0.4.0'); @@ -1040,8 +1040,8 @@ describe('part review tests', () => { const team2 = await createTestTeam(superman.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, batman.userId); - const project1 = await createTestProject(batman, orgId, team1.teamId, car.carId, 0, 1); - const project2 = await createTestProject(superman, orgId, team2.teamId, car.carId, 0, 2); + const project1 = await createTestProject(batman, orgId, team1.teamId, car.carId, 1); + const project2 = await createTestProject(superman, orgId, team2.teamId, car.carId, 2); const proj1WbsNum = validateWBS('0.1.0'); const proj2WbsNum = validateWBS('0.2.0'); diff --git a/src/backend/tests/unit/recruitment.test.ts b/src/backend/tests/unit/recruitment.test.ts index 119f04e78d..bb63387f88 100644 --- a/src/backend/tests/unit/recruitment.test.ts +++ b/src/backend/tests/unit/recruitment.test.ts @@ -3,7 +3,6 @@ import { Organization, User } from '@prisma/client'; import RecruitmentServices from '../../src/services/recruitment.services.js'; import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../../src/utils/errors.utils.js'; import { - createTestGuestDefinition, createTestMilestone, createTestFaq, createTestFAQ, @@ -342,7 +341,6 @@ describe('Recruitment Tests', () => { expect(deletedTestFaq?.dateDeleted).not.toBe(null); }); }); - describe('Create Guest Definitions', () => { it('Successful guest definition creation', async () => { const def = await RecruitmentServices.createGuestDefinition( @@ -379,76 +377,4 @@ describe('Recruitment Tests', () => { ).rejects.toThrow(new AccessDeniedAdminOnlyException('create a guest definition')); }); }); - - describe('Delete Guest Definition', () => { - it('Fails if user is not an admin', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - const guest = await createTestUser(wonderwomanGuest, orgId); - const testDef = await createTestGuestDefinition(admin, orgId); - - await expect( - async () => await RecruitmentServices.deleteGuestDefinition(guest, testDef.definitionId, organization) - ).rejects.toThrow(new AccessDeniedAdminOnlyException('delete a guestDefinition')); - }); - - it('Fails if definition does not exist', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - - await expect( - async () => await RecruitmentServices.deleteGuestDefinition(admin, 'fake-id', organization) - ).rejects.toThrow(new NotFoundException('Guest Definition', 'fake-id')); - }); - - it('Fails if definition is already deleted', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - const testDef = await createTestGuestDefinition(admin, orgId); - await RecruitmentServices.deleteGuestDefinition(admin, testDef.definitionId, organization); - - await expect( - async () => await RecruitmentServices.deleteGuestDefinition(admin, testDef.definitionId, organization) - ).rejects.toThrow(new DeletedException('Guest Definition', testDef.definitionId)); - }); - - it('Successfully deletes a guest definition', async () => { - const admin = await createTestUser(batmanAppAdmin, orgId); - const testDef = await createTestGuestDefinition(admin, orgId); - - await RecruitmentServices.deleteGuestDefinition(admin, testDef.definitionId, organization); - - const deletedTestDef = await prisma.guest_Definition.findUnique({ - where: { definitionId: testDef.definitionId } - }); - - expect(deletedTestDef?.dateDeleted).not.toBe(null); - }); - }); - - describe('Get All Guest Definitions', () => { - it('Succeeds and gets all the guest definitions', async () => { - const def = await RecruitmentServices.createGuestDefinition( - superman, - organization, - 'test term', - 'test description', - 2, - 'iconname', - 'buttonTxt', - 'buttonLink' - ); - - const def2 = await RecruitmentServices.createGuestDefinition( - superman, - organization, - 'test term', - 'test description', - 2, - 'iconname', - 'buttonTxt', - 'buttonLink' - ); - - const result = await RecruitmentServices.getAllGuestDefinitions(organization); - expect(result).toStrictEqual([def, def2]); - }); - }); }); diff --git a/src/backend/tests/unit/reimbursement-requests.test.ts b/src/backend/tests/unit/reimbursement-requests.test.ts index 7f10f8c8e4..0717511bc0 100644 --- a/src/backend/tests/unit/reimbursement-requests.test.ts +++ b/src/backend/tests/unit/reimbursement-requests.test.ts @@ -1,7 +1,7 @@ import { alfred } from '../test-data/users.test-data.js'; import ReimbursementRequestService from '../../src/services/reimbursement-requests.services.js'; import { AccessDeniedException, DeletedException, HttpException, NotFoundException } from '../../src/utils/errors.utils.js'; -import { createTestCar, createTestReimbursementRequest, createTestUser, resetUsers } from '../test-utils.js'; +import { createTestReimbursementRequest, createTestUser, resetUsers } from '../test-utils.js'; import prisma from '../../src/prisma/prisma.js'; import { assert } from 'console'; import { addDaysToDate, IndexCode, ReimbursementRequest, AccountCode, OtherProductReason } from 'shared'; @@ -714,152 +714,6 @@ describe('Reimbursement Requests', () => { }); }); - describe('Car filtering for reimbursement request list endpoints', () => { - let car1RR: ReimbursementRequest; - - beforeEach(async () => { - await createTestCar(org.organizationId, createdUser.userId, 1); - car1RR = await ReimbursementRequestService.createReimbursementRequest( - createdUser, - createdVendor.vendorId, - createdIndexCode.indexCodeId, - [], - [ - { - name: 'BOLT', - reason: { carNumber: 1, projectNumber: 0, workPackageNumber: 0 }, - cost: 500, - refundSources: [{ indexCode: createdIndexCode, amount: 500 }] - } - ], - createdAccountCode.accountCodeId, - 500, - org - ); - }); - - describe('getUserReimbursementRequests', () => { - test('returns only requests whose products belong to the given car', async () => { - const car0Results = await ReimbursementRequestService.getUserReimbursementRequests(createdUser, org, 0); - expect(car0Results.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(car0Results.map((r) => r.reimbursementRequestId)).not.toContain(car1RR.reimbursementRequestId); - - const car1Results = await ReimbursementRequestService.getUserReimbursementRequests(createdUser, org, 1); - expect(car1Results.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - expect(car1Results.map((r) => r.reimbursementRequestId)).not.toContain(reimbursementRequest.reimbursementRequestId); - }); - - test('returns all requests when no carNumber is provided', async () => { - const allResults = await ReimbursementRequestService.getUserReimbursementRequests(createdUser, org); - expect(allResults.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(allResults.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - }); - - test('carNumber 0 filters to only car-0 requests and does not return all requests (Fergus regression)', async () => { - const fergusResults = await ReimbursementRequestService.getUserReimbursementRequests(createdUser, org, 0); - expect(fergusResults).toHaveLength(1); - expect(fergusResults[0].reimbursementRequestId).toBe(reimbursementRequest.reimbursementRequestId); - }); - }); - - describe('getAllReimbursementRequests', () => { - test('returns only requests whose products belong to the given car', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - - const car0Results = await ReimbursementRequestService.getAllReimbursementRequests(financeHead, org, 0); - expect(car0Results.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(car0Results.map((r) => r.reimbursementRequestId)).not.toContain(car1RR.reimbursementRequestId); - - const car1Results = await ReimbursementRequestService.getAllReimbursementRequests(financeHead, org, 1); - expect(car1Results.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - expect(car1Results.map((r) => r.reimbursementRequestId)).not.toContain(reimbursementRequest.reimbursementRequestId); - }); - - test('returns all requests when no carNumber is provided', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - const allResults = await ReimbursementRequestService.getAllReimbursementRequests(financeHead, org); - expect(allResults.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(allResults.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - }); - - test('carNumber 0 filters to only car-0 requests and does not return all requests (Fergus regression)', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - const fergusResults = await ReimbursementRequestService.getAllReimbursementRequests(financeHead, org, 0); - expect(fergusResults).toHaveLength(1); - expect(fergusResults[0].reimbursementRequestId).toBe(reimbursementRequest.reimbursementRequestId); - }); - }); - - describe('getUserAssignedReimbursementRequests', () => { - test('returns only assigned requests whose products belong to the given car', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - const financeMember = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeMember' } }); - await ReimbursementRequestService.assignFinanceMember( - financeHead, - org, - reimbursementRequest.reimbursementRequestId, - financeMember.userId - ); - await ReimbursementRequestService.assignFinanceMember( - financeHead, - org, - car1RR.reimbursementRequestId, - financeMember.userId - ); - - const car0Results = await ReimbursementRequestService.getUserAssignedReimbursementRequests(financeMember, org, 0); - expect(car0Results.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(car0Results.map((r) => r.reimbursementRequestId)).not.toContain(car1RR.reimbursementRequestId); - - const car1Results = await ReimbursementRequestService.getUserAssignedReimbursementRequests(financeMember, org, 1); - expect(car1Results.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - expect(car1Results.map((r) => r.reimbursementRequestId)).not.toContain(reimbursementRequest.reimbursementRequestId); - }); - - test('carNumber 0 filters to only car-0 assigned requests and does not return all (Fergus regression)', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - const financeMember = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeMember' } }); - await ReimbursementRequestService.assignFinanceMember( - financeHead, - org, - reimbursementRequest.reimbursementRequestId, - financeMember.userId - ); - await ReimbursementRequestService.assignFinanceMember( - financeHead, - org, - car1RR.reimbursementRequestId, - financeMember.userId - ); - - const fergusResults = await ReimbursementRequestService.getUserAssignedReimbursementRequests(financeMember, org, 0); - expect(fergusResults).toHaveLength(1); - expect(fergusResults[0].reimbursementRequestId).toBe(reimbursementRequest.reimbursementRequestId); - }); - }); - - describe('getUsersTeamsReimbursementRequests', () => { - test('returns only requests whose products belong to the given car', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - - const car0Results = await ReimbursementRequestService.getUsersTeamsReimbursementRequests(financeHead, org, 0); - expect(car0Results.map((r) => r.reimbursementRequestId)).toContain(reimbursementRequest.reimbursementRequestId); - expect(car0Results.map((r) => r.reimbursementRequestId)).not.toContain(car1RR.reimbursementRequestId); - - const car1Results = await ReimbursementRequestService.getUsersTeamsReimbursementRequests(financeHead, org, 1); - expect(car1Results.map((r) => r.reimbursementRequestId)).toContain(car1RR.reimbursementRequestId); - expect(car1Results.map((r) => r.reimbursementRequestId)).not.toContain(reimbursementRequest.reimbursementRequestId); - }); - - test('carNumber 0 filters to only car-0 requests and does not return all requests (Fergus regression)', async () => { - const financeHead = await prisma.user.findUniqueOrThrow({ where: { googleAuthId: 'financeHead' } }); - const fergusResults = await ReimbursementRequestService.getUsersTeamsReimbursementRequests(financeHead, org, 0); - expect(fergusResults).toHaveLength(1); - expect(fergusResults[0].reimbursementRequestId).toBe(reimbursementRequest.reimbursementRequestId); - }); - }); - }); - describe('Editing an other reimbursement product reason', () => { test('Successfully editing a other reimbursement product reason', async () => { const reason = await ReimbursementRequestService.createOtherReasonReimbursementProduct( diff --git a/src/backend/tests/unit/work-packages.test.ts b/src/backend/tests/unit/work-packages.test.ts deleted file mode 100644 index 542ef26eef..0000000000 --- a/src/backend/tests/unit/work-packages.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Organization, User } from '@prisma/client'; -import prisma from '../../src/prisma/prisma.js'; -import { - createTestCar, - createTestOrganization, - createTestProject, - createTestUser, - createTestWorkPackage, - resetUsers -} from '../test-utils.js'; -import { supermanAdmin } from '../test-data/users.test-data.js'; -import WorkPackagesService from '../../src/services/work-packages.services.js'; - -describe('WorkPackagesService', () => { - let organization: Organization; - let orgId: string; - let user: User; - - beforeEach(async () => { - organization = await createTestOrganization(); - orgId = organization.organizationId; - user = await createTestUser(supermanAdmin, orgId); - }); - - afterEach(async () => { - await resetUsers(); - }); - - describe('getManyWorkPackages', () => { - it('returns all work packages matching the requested WBS numbers', async () => { - const car1 = await createTestCar(orgId, user.userId, 1); - const proj1 = await createTestProject(user, orgId, undefined, car1.carId, 1, 1); - await createTestWorkPackage(user, orgId, proj1.projectId, 1, 1, 1); - await createTestWorkPackage(user, orgId, proj1.projectId, 1, 1, 2); - - const result = await WorkPackagesService.getManyWorkPackages( - [ - { carNumber: 1, projectNumber: 1, workPackageNumber: 1 }, - { carNumber: 1, projectNumber: 1, workPackageNumber: 2 } - ], - organization - ); - - expect(result).toHaveLength(2); - const wpNumbers = result.map((wp) => wp.wbsNum.workPackageNumber).sort((a, b) => a - b); - expect(wpNumbers).toEqual([1, 2]); - }); - }); - - describe('getBlockingWorkPackages', () => { - it('returns work packages that are blocked by the given work package', async () => { - const car1 = await createTestCar(orgId, user.userId, 1); - const proj1 = await createTestProject(user, orgId, undefined, car1.carId, 1, 1); - const wpA = await createTestWorkPackage(user, orgId, proj1.projectId, 1, 1, 1); - const wpB = await createTestWorkPackage(user, orgId, proj1.projectId, 1, 1, 2); - - // wpA blocks wpB - await prisma.work_Package.update({ - where: { workPackageId: wpB.workPackageId }, - data: { blockedBy: { connect: { wbsElementId: wpA.wbsElement.wbsElementId } } } - }); - - const result = await WorkPackagesService.getBlockingWorkPackages( - { carNumber: 1, projectNumber: 1, workPackageNumber: 1 }, - organization - ); - - expect(result).toHaveLength(1); - expect(result[0].wbsNum).toEqual({ carNumber: 1, projectNumber: 1, workPackageNumber: 2 }); - }); - }); -}); diff --git a/src/backend/tests/unmocked/cars.test.ts b/src/backend/tests/unmocked/cars.test.ts deleted file mode 100644 index 99c93b7b5f..0000000000 --- a/src/backend/tests/unmocked/cars.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Organization, User } from '@prisma/client'; -import { createTestCar, createTestOrganization, createTestUser, resetUsers } from '../test-utils'; -import { supermanAdmin, member } from '../test-data/users.test-data'; -import CarsService from '../../src/services/car.services'; -import { AccessDeniedAdminOnlyException } from '../../src/utils/errors.utils'; -import prisma from '../../src/prisma/prisma'; - -describe('Cars Tests', () => { - let org: Organization; - let adminUser: User; - let nonAdminUser: User; - - beforeEach(async () => { - org = await createTestOrganization(); - adminUser = await createTestUser(supermanAdmin, org.organizationId); - nonAdminUser = await createTestUser(member, org.organizationId); - }); - - afterEach(async () => { - await resetUsers(); - }); - - describe('getAllCars', () => { - test('getAllCars returns empty array when no cars exist', async () => { - const cars = await CarsService.getAllCars(org); - expect(cars).toEqual([]); - }); - - test('getAllCars returns all cars for organization', async () => { - await createTestCar(org.organizationId, adminUser.userId, 0); - await createTestCar(org.organizationId, adminUser.userId, 1); - - const cars = await CarsService.getAllCars(org); - expect(cars).toHaveLength(2); - }); - - test('getAllCars only returns cars for specified organization', async () => { - // Create car in our org - await createTestCar(org.organizationId, adminUser.userId, 0); - - // Create car in different org - const uniqueId = `${Date.now()}-${Math.random()}`; - const orgCreator = await prisma.user.create({ - data: { - firstName: 'Org', - lastName: 'Creator', - email: `org-${uniqueId}@test.com`, - googleAuthId: `org-${uniqueId}` - } - }); - - const otherOrg = await prisma.organization.create({ - data: { - name: 'Other Org', - description: 'Other organization', - applicationLink: '', - userCreatedId: orgCreator.userId - } - }); - - const otherUser = await createTestUser( - { - ...supermanAdmin, - googleAuthId: `admin-${uniqueId}`, - email: `admin-${uniqueId}@test.com`, - emailId: `admin-${uniqueId}` - }, - otherOrg.organizationId - ); - - await createTestCar(otherOrg.organizationId, otherUser.userId, 0); - - const cars = await CarsService.getAllCars(org); - expect(cars).toHaveLength(1); - }); - }); - - describe('createCar', () => { - test('createCar successfully creates car with admin permissions', async () => { - const carName = 'Test Car'; - - const createdCar = await CarsService.createCar(org, adminUser, carName); - - expect(createdCar.name).toBe(carName); - expect(createdCar.wbsNum.carNumber).toBe(0); // First car should have car number 0 - expect(createdCar.wbsNum.projectNumber).toBe(0); - expect(createdCar.wbsNum.workPackageNumber).toBe(0); - }); - - test('createCar assigns correct car number based on existing cars', async () => { - // Create first car - await CarsService.createCar(org, adminUser, 'Car 1'); - - // Create second car - const secondCar = await CarsService.createCar(org, adminUser, 'Car 2'); - - expect(secondCar.wbsNum.carNumber).toBe(1); // Should be incremented - }); - - test('createCar throws AccessDeniedAdminOnlyException for non-admin user', async () => { - await expect(CarsService.createCar(org, nonAdminUser, 'Test Car')).rejects.toThrow(AccessDeniedAdminOnlyException); - }); - - test('createCar car numbers are organization-specific', async () => { - // Create car in first org - const firstCar = await CarsService.createCar(org, adminUser, 'First Org Car'); - - // Create different org and admin - const uniqueId = `${Date.now()}-${Math.random()}`; - const orgCreator = await prisma.user.create({ - data: { - firstName: 'Org', - lastName: 'Creator', - email: `org2-${uniqueId}@test.com`, - googleAuthId: `org2-${uniqueId}` - } - }); - - const otherOrg = await prisma.organization.create({ - data: { - name: 'Second Org', - description: 'Second organization', - applicationLink: '', - userCreatedId: orgCreator.userId - } - }); - - const otherAdmin = await createTestUser( - { - ...supermanAdmin, - googleAuthId: `admin2-${uniqueId}`, - email: `admin2-${uniqueId}@test.com`, - emailId: `admin2-${uniqueId}` - }, - otherOrg.organizationId - ); - - // Create car in second org - const secondCar = await CarsService.createCar(otherOrg, otherAdmin, 'Second Org Car'); - - // Both should start from car number 0 - expect(firstCar.wbsNum.carNumber).toBe(0); - expect(secondCar.wbsNum.carNumber).toBe(0); - }); - }); -}); diff --git a/src/backend/tests/unmocked/project.test.ts b/src/backend/tests/unmocked/project.test.ts index 1135a8246c..36132df7f4 100644 --- a/src/backend/tests/unmocked/project.test.ts +++ b/src/backend/tests/unmocked/project.test.ts @@ -52,7 +52,6 @@ describe('Material Tests', () => { expect(material.manufacturerName).toEqual('Digikey'); expect(material.manufacturerPartNumber).toEqual('lalsd'); expect(material.quantity?.toString()).toEqual('5'); - expect(material.isCopied).toBe(false); }); }); @@ -149,9 +148,6 @@ describe('Material Tests', () => { expect(copiedMat1.notes).toBe('Test notes'); expect(copiedMat2.status).toBe('NOT_READY_TO_ORDER'); - - expect(copiedMat1.isCopied).toBe(true); - expect(copiedMat2.isCopied).toBe(true); }); test('Fails when material does not exist', async () => { diff --git a/src/backend/tests/unmocked/statistics.test.ts b/src/backend/tests/unmocked/statistics.test.ts index aca04c4bce..0e6cf7fe4c 100644 --- a/src/backend/tests/unmocked/statistics.test.ts +++ b/src/backend/tests/unmocked/statistics.test.ts @@ -92,7 +92,7 @@ describe('Statistics Tests', () => { const team = await createTestTeam(user.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, user.userId); await createTestProject(user, orgId, team.teamId, car.carId); - await createTestProject(user, orgId, team.teamId, car.carId, 0, 2); + await createTestProject(user, orgId, team.teamId, car.carId, 2); const result = await StatisticsService.createGraph( user, @@ -131,7 +131,7 @@ describe('Statistics Tests', () => { const team = await createTestTeam(user.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, user.userId); await createTestProject(user, orgId, team.teamId, car.carId); - await createTestProject(user, orgId, team.teamId, car.carId, 0, 2); + await createTestProject(user, orgId, team.teamId, car.carId, 2); const result = await StatisticsService.createGraph( user, @@ -173,7 +173,7 @@ describe('Statistics Tests', () => { const team = await createTestTeam(user.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, user.userId); await createTestProject(user, orgId, team.teamId, car.carId); - await createTestProject(user, orgId, team.teamId, car.carId, 0, 2, new Date()); + await createTestProject(user, orgId, team.teamId, car.carId, 2, new Date()); const result = await StatisticsService.createGraph( user, @@ -215,7 +215,7 @@ describe('Statistics Tests', () => { const team = await createTestTeam(user.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, user.userId); await createTestProject(user, orgId, team.teamId, car.carId); - await createTestProject(user, orgId, team.teamId, car.carId, 0, 2, new Date()); + await createTestProject(user, orgId, team.teamId, car.carId, 2, new Date()); const result = await StatisticsService.createGraph( user, @@ -255,7 +255,7 @@ describe('Statistics Tests', () => { const team = await createTestTeam(user.userId, division.teamTypeId, orgId); const car = await createTestCar(orgId, user.userId); await createTestProject(user, orgId, team.teamId, car.carId); - await createTestProject(user, orgId, team.teamId, car.carId, 0, 2, new Date()); + await createTestProject(user, orgId, team.teamId, car.carId, 2, new Date()); const result = await StatisticsService.createGraph( user, diff --git a/src/docs-site/README.md b/src/docs-site/README.md index 469a5cd017..e4a9838f95 100644 --- a/src/docs-site/README.md +++ b/src/docs-site/README.md @@ -246,7 +246,7 @@ If your changes aren't appearing: Common issues: -- **Node version**: Ensure you're running Node.js 18 or higher (although you should be running node 25 for the main site development) +- **Node version**: Ensure you're running Node.js 18 or higher - **Dependencies**: From the root directory, try `rm -rf docs-site/node_modules && yarn install` - **Port conflict**: Docs run on port 3002. If it's in use, Docusaurus will try another port or you can stop the conflicting process diff --git a/src/docs-site/scripts/sync-skills.mjs b/src/docs-site/scripts/sync-skills.mjs index c707431bbf..bc905547c0 100644 --- a/src/docs-site/scripts/sync-skills.mjs +++ b/src/docs-site/scripts/sync-skills.mjs @@ -46,7 +46,7 @@ function parseFrontmatter(content) { const restOfContent = content.slice(match[0].length); // Parse YAML - const lines = yamlContent.split(/\r?\n/); + const lines = yamlContent.split('\n'); const metadata = {}; let currentKey = null; let currentValue = ''; diff --git a/src/frontend/package.json b/src/frontend/package.json index bf46d366d4..989879471e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -58,7 +58,7 @@ "@testing-library/react": "^16.2.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.6.0", - "@types/node": "^25.0.0", + "@types/node": "20.0.0", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "@types/react-helmet": "^6.1.6", diff --git a/src/frontend/src/apis/attendance.api.ts b/src/frontend/src/apis/attendance.api.ts deleted file mode 100644 index 95fe6aacf3..0000000000 --- a/src/frontend/src/apis/attendance.api.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from '../utils/axios'; -import { apiUrls } from '../utils/urls'; -import { MeetingAttendance, MeetingAttendanceWithAttendees } from 'shared'; - -export const postTakeAttendance = (payload: { teamId: string; message: string }) => { - return axios.post(apiUrls.attendanceTakeAttendance(), payload, { - transformResponse: (data) => JSON.parse(data) as MeetingAttendance - }); -}; - -export const getAllAttendances = () => { - return axios.get(apiUrls.attendanceGetAll(), { - transformResponse: (data) => JSON.parse(data) as MeetingAttendance[] - }); -}; - -export const getCheckChannelName = (teamId: string) => { - return axios.get<{ channelName: string | undefined; valid: boolean }>(apiUrls.attendanceCheckChannel(teamId), { - transformResponse: (data) => JSON.parse(data) as { channelName: string | undefined; valid: boolean } - }); -}; - -export const getOngoingAttendance = (teamId: string) => { - return axios.get(apiUrls.attendanceGetOngoing(teamId), { - transformResponse: (data) => JSON.parse(data) as MeetingAttendance | null - }); -}; - -export const postCloseAttendance = (teamId: string) => { - return axios.post(apiUrls.attendanceCloseOngoing(teamId)); -}; - -export const getAttendanceById = (meetingAttendanceId: string) => { - return axios.get(apiUrls.attendanceGetById(meetingAttendanceId), { - transformResponse: (data) => JSON.parse(data) as MeetingAttendanceWithAttendees - }); -}; diff --git a/src/frontend/src/apis/calendar.api.ts b/src/frontend/src/apis/calendar.api.ts index 1064bf8eb2..338a329db7 100644 --- a/src/frontend/src/apis/calendar.api.ts +++ b/src/frontend/src/apis/calendar.api.ts @@ -11,8 +11,7 @@ import { EventTypeCreateArgs, Calendar, FilterArgs, - ScheduleSlot, - EventInstance + ScheduleSlot } from 'shared'; import { eventTransformer, eventWithMembersTransformer } from './transformers/calendar.transformer'; import { EditEventArgs, EditScheduleSlotArgs, EventCreateArgs } from '../hooks/calendar.hooks'; @@ -267,19 +266,3 @@ export const scheduleEvent = async (eventId: string, payload: { startTime: Date; transformResponse: (data) => eventTransformer(JSON.parse(data)) }); }; - -export const getAllEventsPaginated = (cursor?: Date, pageSize?: number) => { - return axios.post<{ instances: EventInstance[]; nextCursor: Date | null }>( - apiUrls.calendarEventsPaginated(), - { cursor, pageSize }, - { - transformResponse: (data) => { - const parsed = JSON.parse(data); - return { - instances: parsed.instances, - nextCursor: parsed.nextCursor ? new Date(parsed.nextCursor) : null - }; - } - } - ); -}; diff --git a/src/frontend/src/apis/change-requests.api.ts b/src/frontend/src/apis/change-requests.api.ts index 9892caddeb..7f101a45de 100644 --- a/src/frontend/src/apis/change-requests.api.ts +++ b/src/frontend/src/apis/change-requests.api.ts @@ -4,7 +4,7 @@ */ import axios from '../utils/axios'; -import { ChangeRequest, WbsNumber, ChangeRequestType, GuestChangeRequest } from 'shared'; +import { ChangeRequest, WbsNumber, ChangeRequestType } from 'shared'; import { apiUrls } from '../utils/urls'; import { changeRequestTransformer } from './transformers/change-requests.transformers'; import { CreateStandardChangeRequestPayload } from '../hooks/change-requests.hooks'; @@ -18,12 +18,6 @@ export const getAllChangeRequests = () => { }); }; -export const getAllGuestChangeRequests = () => { - return axios.get(apiUrls.guestChangeRequests(), { - transformResponse: (data) => JSON.parse(data) - }); -}; - export const getToReviewChangeRequests = () => { return axios.get(apiUrls.toReviewChangeRequests(), { transformResponse: (data) => JSON.parse(data).map(changeRequestTransformer) diff --git a/src/frontend/src/apis/finance.api.ts b/src/frontend/src/apis/finance.api.ts index ee9a724885..c6130e7bea 100644 --- a/src/frontend/src/apis/finance.api.ts +++ b/src/frontend/src/apis/finance.api.ts @@ -327,7 +327,7 @@ export const downloadBlobsToPdf = async (blobData: Blob[], filename: string) => break; } default: { - console.log(blob.type + ' type not supported and will not be added to the PDF'); + console.log(blob.type + 'type not supported and will not be added to the PDF, name: ' + blob.name); // throw new Error(blob.type + ' type not supported'); } } @@ -664,9 +664,8 @@ export const getReimbursementRequestProjectData = (payload: ReimbursementRequest export const getReimbursementRequestTeamData = (payload: ReimbursementRequestTeamDataPayload) => { return axios.get( - apiUrls.getReimbursementRequestTeamData(payload.teamId, payload.startDate, payload.endDate), + apiUrls.getReimbursementRequestTeamData(payload.teamId, payload.startDate, payload.endDate, payload.carNumber), { - overrideCarId: payload.overrideCarId, transformResponse: (data) => reimbursementRequestDataTransformer(JSON.parse(data)) } ); @@ -674,58 +673,65 @@ export const getReimbursementRequestTeamData = (payload: ReimbursementRequestTea export const getReimbursementRequestCategoryData = (payload: ReimbursementRequestCategoryDataPayload) => { return axios.get( - apiUrls.getReimbursementRequestCategoryData(payload.otherReasonId, payload.startDate, payload.endDate), + apiUrls.getReimbursementRequestCategoryData( + payload.otherReasonId, + payload.startDate, + payload.endDate, + payload.carNumber + ), { - overrideCarId: payload.overrideCarId, transformResponse: (data) => reimbursementRequestDataTransformer(JSON.parse(data)) } ); }; export const getAllReimbursementRequestData = (payload: ReimbursementRequestDataPayload) => { - return axios.get(apiUrls.getAllReimbursementRequestData(payload.startDate, payload.endDate), { - overrideCarId: payload.overrideCarId, - transformResponse: (data) => JSON.parse(data).map(reimbursementRequestDataTransformer) - }); + return axios.get( + apiUrls.getAllReimbursementRequestData(payload.startDate, payload.endDate, payload.carNumber), + { + transformResponse: (data) => JSON.parse(data).map(reimbursementRequestDataTransformer) + } + ); }; export const getReimbursementRequestTeamTypeData = (payload: ReimbursementRequestTeamTypeDataPayload) => { return axios.get( - apiUrls.getReimbursementRequestTeamTypeData(payload.teamTypeId, payload.startDate, payload.endDate), + apiUrls.getReimbursementRequestTeamTypeData(payload.teamTypeId, payload.startDate, payload.endDate, payload.carNumber), { - overrideCarId: payload.overrideCarId, transformResponse: (data) => reimbursementRequestDataTransformer(JSON.parse(data)) } ); }; export const getSpendingBarTeamData = (payload: SpendingBarTeamDataPayload) => { - return axios.get(apiUrls.getSpendingBarTeamData(payload.teamId, payload.startDate, payload.endDate), { - overrideCarId: payload.overrideCarId, - transformResponse: (data) => spendingBarDataTransformer(JSON.parse(data)) - }); + return axios.get( + apiUrls.getSpendingBarTeamData(payload.teamId, payload.startDate, payload.endDate, payload.carNumber), + { + transformResponse: (data) => spendingBarDataTransformer(JSON.parse(data)) + } + ); }; export const getSpendingBarTeamTypeData = (payload: SpendingBarTeamTypeDataPayload) => { return axios.get( - apiUrls.getSpendingBarTeamTypeData(payload.teamTypeId, payload.startDate, payload.endDate), + apiUrls.getSpendingBarTeamTypeData(payload.teamTypeId, payload.startDate, payload.endDate, payload.carNumber), { - overrideCarId: payload.overrideCarId, transformResponse: (data) => JSON.parse(data).map(spendingBarDataTransformer) } ); }; export const getSpendingBarCategoryData = (payload: SpendingBarDataPayload) => { - return axios.get(apiUrls.getSpendingBarCategoryData(payload.startDate, payload.endDate), { - overrideCarId: payload.overrideCarId, - transformResponse: (data) => spendingBarDataTransformer(JSON.parse(data)) - }); + return axios.get( + apiUrls.getSpendingBarCategoryData(payload.startDate, payload.endDate, payload.carNumber), + { + transformResponse: (data) => spendingBarDataTransformer(JSON.parse(data)) + } + ); }; export const getAllSpendingBarData = (payload: SpendingBarDataPayload) => { - return axios.get(apiUrls.getAllSpendingBarData(payload.startDate, payload.endDate), { - overrideCarId: payload.overrideCarId, + return axios.get(apiUrls.getAllSpendingBarData(payload.startDate, payload.endDate, payload.carNumber), { transformResponse: (data) => JSON.parse(data).map(spendingBarDataTransformer) }); }; diff --git a/src/frontend/src/apis/projects.api.ts b/src/frontend/src/apis/projects.api.ts index aad9b5a31a..e5d3ce62a4 100644 --- a/src/frontend/src/apis/projects.api.ts +++ b/src/frontend/src/apis/projects.api.ts @@ -27,14 +27,11 @@ import { import { CreateSingleProjectPayload, EditSingleProjectPayload } from '../utils/types'; /** - * Fetches all projects with query args needed for Gantt chart - * Note: Gantt supports multi-car local selection and handles its own frontend filtering, - * so we bypass the global car filter using overrideCarId: 'all-cars' + * Fetches all projects with querry args needed for Gantt chart */ export const getAllProjectsGantt = () => { return axios.get(apiUrls.allProjectsGantt(), { - transformResponse: (data) => JSON.parse(data).map(projectGanttTransformer), - overrideCarId: 'all-cars' + transformResponse: (data) => JSON.parse(data).map(projectGanttTransformer) }); }; diff --git a/src/frontend/src/apis/transformers/projects.transformers.ts b/src/frontend/src/apis/transformers/projects.transformers.ts index 78ef0f5cd7..280bdcdfa1 100644 --- a/src/frontend/src/apis/transformers/projects.transformers.ts +++ b/src/frontend/src/apis/transformers/projects.transformers.ts @@ -99,7 +99,8 @@ export const projectOverviewTransformer = (project: ProjectOverview): ProjectOve startDate: new Date(wp.startDate), endDate: new Date(wp.endDate) })), - links: project.links + links: project.links, + tasks: project.tasks.map(taskTransformer) }; }; diff --git a/src/frontend/src/app/AppAuthenticated.tsx b/src/frontend/src/app/AppAuthenticated.tsx index c67546853c..5c4bfbd948 100644 --- a/src/frontend/src/app/AppAuthenticated.tsx +++ b/src/frontend/src/app/AppAuthenticated.tsx @@ -21,21 +21,81 @@ import LoadingIndicator from '../components/LoadingIndicator'; import SessionTimeoutAlert from './SessionTimeoutAlert'; import SetUserPreferences from '../pages/HomePage/components/SetUserPreferences'; import Finance from '../pages/FinancePage/Finance'; +import Sidebar from '../layouts/Sidebar/Sidebar'; +import { Box } from '@mui/system'; +import { Container, IconButton, useTheme } from '@mui/material'; import ErrorPage from '../pages/ErrorPage'; import { Role, isGuest } from 'shared'; +import { useState } from 'react'; +import ArrowCircleRightTwoToneIcon from '@mui/icons-material/ArrowCircleRightTwoTone'; +import HiddenContentMargin from '../components/HiddenContentMargin'; +import { useHomePageContext } from './HomePageContext'; import { useCurrentOrganization } from '../hooks/organizations.hooks'; -import { GlobalCarFilterProvider } from './AppGlobalCarFilterContext'; import Statistics from '../pages/StatisticsPage/Statistics'; import RetrospectiveGanttChartPage from '../pages/RetrospectivePage/Retrospective'; import Calendar from '../pages/CalendarPage/Calendar'; -import GuestEventPage from '../pages/GuestEventPage/GuestEventPage'; -import SidebarLayout from '../layouts/SidebarLayout'; interface AppAuthenticatedProps { userId: string; userRole: Role; } +const SidebarLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const theme = useTheme(); + const [drawerOpen, setDrawerOpen] = useState(false); + const [moveContent, setMoveContent] = useState(false); + const { onGuestHomePage } = useHomePageContext(); + + return ( + <> + { + setDrawerOpen(true); + }} + sx={{ + height: '100vh', + position: 'fixed', + width: 15, + borderRight: 2, + borderRightColor: theme.palette.background.paper + }} + /> + { + setDrawerOpen(true); + setMoveContent(true); + }} + sx={{ position: 'fixed', left: -8, top: '3%' }} + id="sidebar-button" + > + + + + + + + {children} + + + + ); +}; + const AppAuthenticated: React.FC = ({ userId, userRole }) => { const { isLoading, isError, error, data: userSettingsData } = useSingleUserSettings(userId); @@ -59,36 +119,31 @@ const AppAuthenticated: React.FC = ({ userId, userRole }) return ; } - return ( - - {userSettingsData.slackId || isGuest(userRole) ? ( - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - - )} - + return userSettingsData.slackId || isGuest(userRole) ? ( + + + + + + + + + + + + + + + + + + + + + + + ) : ( + ); }; diff --git a/src/frontend/src/app/AppGlobalCarFilterContext.tsx b/src/frontend/src/app/AppGlobalCarFilterContext.tsx deleted file mode 100644 index 802dadfb65..0000000000 --- a/src/frontend/src/app/AppGlobalCarFilterContext.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import React, { createContext, useContext, useState, useEffect, useLayoutEffect, ReactNode } from 'react'; -import { Car } from 'shared'; -import { useGetAllCars } from '../hooks/cars.hooks'; -import { setCurrentCarId } from '../utils/axios'; -import LoadingIndicator from '../components/LoadingIndicator'; - -interface GlobalCarFilterContextType { - selectedCar: Car | 'all-cars'; - allCars: Car[]; - setSelectedCar: (car: Car | 'all-cars') => void; - isLoading: boolean; - isInitialized: boolean; - error: Error | null; -} - -const GlobalCarFilterContext = createContext(undefined); - -interface GlobalCarFilterProviderProps { - children: ReactNode; -} - -export const GlobalCarFilterProvider: React.FC = ({ children }) => { - const [selectedCar, setSelectedCarState] = useState('all-cars'); - const [isInitialized, setIsInitialized] = useState(false); - - const { data: allCars = [], isLoading, error } = useGetAllCars(); - - // Guarantees the header is updated before React Query enqueues new fetches. - useLayoutEffect(() => { - setCurrentCarId(selectedCar === 'all-cars' ? null : selectedCar.id); - }, [selectedCar]); - - useEffect(() => { - if (!isLoading && !isInitialized) { - const savedCarId = localStorage.getItem('selectedCarId'); - - // Handle saved selection - if (savedCarId === 'all-cars') { - setSelectedCarState('all-cars'); - setIsInitialized(true); - return; - } else if (savedCarId) { - const savedCar = allCars.find((car) => car.id === savedCarId); - if (savedCar) { - setSelectedCarState(savedCar); - setIsInitialized(true); - return; - } - // Fall back to default if saved car id is invalid - localStorage.removeItem('selectedCarId'); - } - - // Default to most recent car if no car was previously saved (highest car number) - const mostRecentCar = - allCars.length > 0 ? allCars.reduce((a, b) => (a.wbsNum.carNumber > b.wbsNum.carNumber ? a : b)) : null; - if (mostRecentCar) { - setSelectedCarState(mostRecentCar); - localStorage.setItem('selectedCarId', mostRecentCar.id); - } else { - setSelectedCarState('all-cars'); - } - setIsInitialized(true); - } - }, [allCars, isLoading, isInitialized]); - - const setSelectedCar = (car: Car | 'all-cars') => { - setSelectedCarState(car); - if (car !== 'all-cars') { - localStorage.setItem('selectedCarId', car.id); - } else { - localStorage.setItem('selectedCarId', 'all-cars'); - } - }; - - const value: GlobalCarFilterContextType = { - selectedCar, - allCars, - setSelectedCar, - isLoading, - isInitialized, - error - }; - - return ( - - {isInitialized ? children : } - - ); -}; - -export const useGlobalCarFilter = (): GlobalCarFilterContextType => { - const context = useContext(GlobalCarFilterContext); - if (context === undefined) { - throw new Error('useGlobalCarFilter must be used within a GlobalCarFilterProvider'); - } - return context; -}; diff --git a/src/frontend/src/components/DrawerHeader.tsx b/src/frontend/src/components/DrawerHeader.tsx index dc6931b576..01947a5311 100644 --- a/src/frontend/src/components/DrawerHeader.tsx +++ b/src/frontend/src/components/DrawerHeader.tsx @@ -7,8 +7,12 @@ import { styled } from '@mui/material'; const DrawerHeader = styled('div')(({ theme }) => ({ display: 'flex', - alignItems: 'flex-start', - padding: theme.spacing(0, 1) + alignItems: 'center', + justifyContent: 'flex-end', + padding: theme.spacing(0, 1), + height: '68px', + // necessary for content to be below app bar + ...theme.mixins.toolbar })); export default DrawerHeader; diff --git a/src/frontend/src/components/FinanceDashboardCarFilter.tsx b/src/frontend/src/components/FinanceDashboardCarFilter.tsx deleted file mode 100644 index 26bfe54ebe..0000000000 --- a/src/frontend/src/components/FinanceDashboardCarFilter.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import React from 'react'; -import { Box, Tooltip, Typography, FormControl, FormLabel } from '@mui/material'; -import { HelpOutline as HelpIcon } from '@mui/icons-material'; -import { DatePicker } from '@mui/x-date-pickers'; -import NERAutocomplete from './NERAutocomplete'; -import type { FinanceDashboardCarFilter as FinanceDashboardCarFilterType } from '../hooks/finance-car-filter.hooks'; - -interface FinanceDashboardCarFilterProps { - filter: FinanceDashboardCarFilterType; - sx?: object; - size?: 'small' | 'medium'; - controlSx?: object; -} - -const ALL_CARS_ID = '__ALL_CARS__'; -const ALL_CARS_OPTION = { label: 'All Cars', id: ALL_CARS_ID, carNumber: -1 }; - -const inputStyle = { - '.MuiInputBase-root': { - height: '36px', - padding: '0 8px', - backgroundColor: '#ef4345', - color: 'white', - fontSize: '13px', - borderRadius: '4px', - '&:hover': { backgroundColor: '#ef4345' }, - '&.Mui-focused': { backgroundColor: '#ef4345', color: 'white' } - }, - '& .MuiInputBase-input': { - color: 'white', - paddingTop: '8px', - cursor: 'pointer', - '&:focus': { color: 'white' } - }, - '& .MuiOutlinedInput-notchedOutline': { - border: '1px solid #fff', - '&:hover': { borderColor: '#fff' }, - '&.Mui-focused': { borderColor: '#fff' } - }, - '& .MuiSvgIcon-root': { - color: 'white', - '&:hover': { color: 'white' }, - '&.Mui-focused': { color: 'white' } - } -}; - -const labelStyle = { display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5, color: 'white' }; - -const FinanceDashboardCarFilterComponent: React.FC = ({ - filter, - sx = {}, - size = 'small', - controlSx = {} -}) => { - const { - selectedCar, - allCars, - startDate, - endDate, - setSelectedCar, - clearLocalSelection, - setStartDate, - setEndDate, - isLoading - } = filter; - - const sortedCars = [...allCars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber); - - const carOptions = sortedCars.map((car) => ({ - label: car.wbsNum.carNumber === 0 ? car.name : `${car.name} (Car ${car.wbsNum.carNumber})`, - id: car.id, - carNumber: car.wbsNum.carNumber - })); - - const carAutocompleteOptions = [ALL_CARS_OPTION, ...carOptions]; - - const handleCarChange = (_event: React.SyntheticEvent, newValue: { label: string; id: string } | null) => { - if (newValue === null) { - // User cleared the input (X button), re-mirror global - clearLocalSelection(); - } else if (newValue.id === ALL_CARS_ID) { - // Explicit "All Cars" override, bypass global filter entirely - setSelectedCar('all-cars'); - } else { - const car = allCars.find((c) => c.id === newValue.id); - if (car) setSelectedCar(car); - } - }; - - const selectedCarOption = - selectedCar === 'all-cars' ? ALL_CARS_OPTION : (carOptions.find((option) => option.id === selectedCar.id) ?? null); - - if (isLoading) { - return ( - - Loading car data... - - ); - } - - return ( - - - - Car Filter - - - - - - - - - - Start Date - - - - - (endDate ? date > endDate : false)} - slotProps={{ - textField: { size, sx: { width: 180, ...inputStyle, ...controlSx } }, - field: { clearable: true } - }} - onChange={(newValue: Date | null) => setStartDate(newValue ?? undefined)} - /> - - - - - End Date - - - - - (startDate ? date < startDate : false)} - slotProps={{ - textField: { size, sx: { width: 180, ...inputStyle, ...controlSx } }, - field: { clearable: true } - }} - onChange={(newValue: Date | null) => setEndDate(newValue ?? undefined)} - /> - - - ); -}; - -export default FinanceDashboardCarFilterComponent; diff --git a/src/frontend/src/components/FullPageTabs.tsx b/src/frontend/src/components/FullPageTabs.tsx index fd2eed6ee9..32bb44b55d 100644 --- a/src/frontend/src/components/FullPageTabs.tsx +++ b/src/frontend/src/components/FullPageTabs.tsx @@ -14,18 +14,9 @@ interface TabProps { defaultTab: string; //tab that the tabs component defaults to id: string; noUnderline?: boolean; - scrollable?: boolean; } -const FullPageTabs = ({ - setTab, - tabsLabels, - baseUrl, - defaultTab, - id, - noUnderline = false, - scrollable = false -}: TabProps) => { +const FullPageTabs = ({ setTab, tabsLabels, baseUrl, defaultTab, id, noUnderline = false }: TabProps) => { const tabUrlValues = tabsLabels.map((tab) => tab.tabUrlValue); const match = useRouteMatch<{ tabValueString: string }>(`${baseUrl}/:tabValueString`); const tabValueString = match?.params?.tabValueString; @@ -46,13 +37,7 @@ const FullPageTabs = ({ }; return ( - + {tabsLabels.map((tab, idx) => ( = ({ sx = {} }) => { - const { selectedCar, allCars, setSelectedCar, isLoading, error } = useGlobalCarFilter(); - - if (isLoading || error) return null; - - if (allCars.length === 0) { - return ( - - - No cars available - - - ); - } - - const sortedCars = [...allCars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber); - - return ( - - setSelectedCar('all-cars')} - variant="outlined" - sx={{ - borderColor: 'white', - color: 'white', - backgroundColor: 'transparent', - fontWeight: selectedCar === 'all-cars' ? 'bold' : 'normal', - borderWidth: selectedCar === 'all-cars' ? 2 : 1, - '&:hover': { backgroundColor: 'rgba(255,255,255,0.1)' }, - whiteSpace: 'nowrap' - }} - /> - {sortedCars.map((car) => { - const isSelected = selectedCar !== 'all-cars' && selectedCar.id === car.id; - return ( - setSelectedCar(car)} - variant="outlined" - sx={{ - borderColor: 'white', - color: 'white', - backgroundColor: 'transparent', - fontWeight: isSelected ? 'bold' : 'normal', - borderWidth: isSelected ? 2 : 1, - '&:hover': { backgroundColor: 'rgba(255,255,255,0.1)' }, - whiteSpace: 'nowrap' - }} - /> - ); - })} - - ); -}; - -export default GlobalCarFilterChips; diff --git a/src/frontend/src/components/GlobalCarFilterDropdown.tsx b/src/frontend/src/components/GlobalCarFilterDropdown.tsx deleted file mode 100644 index 4a0c9e6aa8..0000000000 --- a/src/frontend/src/components/GlobalCarFilterDropdown.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { Box } from '@mui/material'; -import GlobalCarFilterHeader from './GlobalCarFilterHeader'; -import GlobalCarFilterChips from './GlobalCarFilterChips'; - -interface GlobalCarFilterDropdownProps { - sx?: object; -} - -const GlobalCarFilterDropdown: React.FC = ({ sx = {} }) => { - return ( - - - - - ); -}; - -export default GlobalCarFilterDropdown; diff --git a/src/frontend/src/components/GlobalCarFilterHeader.tsx b/src/frontend/src/components/GlobalCarFilterHeader.tsx deleted file mode 100644 index f85acadff1..0000000000 --- a/src/frontend/src/components/GlobalCarFilterHeader.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { Box, Typography } from '@mui/material'; -import { DirectionsCar as CarIcon } from '@mui/icons-material'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; -import LoadingIndicator from './LoadingIndicator'; - -interface GlobalCarFilterHeaderProps { - sx?: object; -} - -const GlobalCarFilterHeader: React.FC = ({ sx = {} }) => { - const { selectedCar, allCars, isLoading, error } = useGlobalCarFilter(); - - if (isLoading) return ; - - if (error) { - return ( - - - {error.message} - - - ); - } - - if (allCars.length === 0) return null; - - const currentCarLabel = selectedCar === 'all-cars' ? 'All Cars' : selectedCar.name; - - return ( - - - - - Working with: - - - {currentCarLabel} - - - - ); -}; - -export default GlobalCarFilterHeader; diff --git a/src/frontend/src/components/NERFormModal.tsx b/src/frontend/src/components/NERFormModal.tsx index 011f9d3d7c..47422200ed 100644 --- a/src/frontend/src/components/NERFormModal.tsx +++ b/src/frontend/src/components/NERFormModal.tsx @@ -1,7 +1,6 @@ import { ReactNode } from 'react'; import { FieldValues, UseFormHandleSubmit, UseFormReset } from 'react-hook-form'; import NERModal, { NERModalProps } from './NERModal'; -import { useToast } from '../hooks/toasts.hooks'; interface NERFormModalProps extends NERModalProps { reset: UseFormReset; @@ -32,23 +31,18 @@ const NERFormModal = ({ titleChildren, actionsLeftChildren }: NERFormModalProps) => { - const toast = useToast(); /** * Wrapper function for onSubmit so that form data is reset after submit */ const onSubmitWrapper = async (data: any) => { - try { - await onFormSubmit(data); - reset(); - } catch (e: unknown) { - if (e instanceof Error) toast.error(e.message, 6000); - } + await onFormSubmit(data); + reset(); }; - const handleFormSubmit = async (e: React.FormEvent) => { + const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); e.stopPropagation(); // Prevent event bubbling - await handleUseFormSubmit(onSubmitWrapper)(e); + handleUseFormSubmit(onSubmitWrapper)(e); }; return ( diff --git a/src/frontend/src/components/ProjectDetailCard.tsx b/src/frontend/src/components/ProjectDetailCard.tsx index 9cc1fd77c4..3efafb772c 100644 --- a/src/frontend/src/components/ProjectDetailCard.tsx +++ b/src/frontend/src/components/ProjectDetailCard.tsx @@ -8,7 +8,7 @@ import AttachMoneyIcon from '@mui/icons-material/AttachMoney'; import ScheduleIcon from '@mui/icons-material/Schedule'; import { Box, Card, CardContent, Link, Typography, Grid } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; -import { calculateDaysLeftInProject, daysBetween, ProjectOverview, WbsElementStatus, wbsPipe } from 'shared'; +import { calculateDaysLeftInProject, daysBetween, ProjectOverview, TaskStatus, WbsElementStatus, wbsPipe } from 'shared'; import { daysOrWeeksLeftOrLate, emDashPipe, fullNamePipe } from '../utils/pipes'; import WorkPackageStageChip from './WorkPackageStageChip'; import FavoriteProjectButton from './FavoriteProjectButton'; @@ -23,6 +23,7 @@ interface ProjectDetailCardProps { const ProjectDetailCard: React.FC = ({ project, projectIsFavorited }) => { const containsActiveWorkPackages = project.workPackages.filter((wp) => wp.status === WbsElementStatus.Active).length; + const tasksLeft: number = project.tasks.filter((task) => task.status !== TaskStatus.DONE).length; const ProjectDetailCardTitle = () => ( @@ -62,8 +63,7 @@ const ProjectDetailCard: React.FC = ({ project, projectI )} - {' '} - {`${project.tasksRemaining} task${project.tasksRemaining === 1 ? '' : 's'} left`} + {`${tasksLeft} task${tasksLeft === 1 ? '' : 's'} left`} {fullNamePipe(project.manager)} diff --git a/src/frontend/src/hooks/attendance.hooks.ts b/src/frontend/src/hooks/attendance.hooks.ts deleted file mode 100644 index 95934659c1..0000000000 --- a/src/frontend/src/hooks/attendance.hooks.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { MeetingAttendance, MeetingAttendanceWithAttendees } from 'shared'; -import { - getAllAttendances, - getAttendanceById, - getCheckChannelName, - getOngoingAttendance, - postCloseAttendance, - postTakeAttendance -} from '../apis/attendance.api'; - -const ATTENDANCE_KEY = ['attendances'] as const; - -export const useTakeAttendance = () => { - const queryClient = useQueryClient(); - return useMutation( - ({ teamId, message }) => postTakeAttendance({ teamId, message }).then((res) => res.data), - { - onSuccess: (_, { teamId }) => { - queryClient.invalidateQueries(ATTENDANCE_KEY); - queryClient.invalidateQueries(['attendance-ongoing', teamId]); - } - } - ); -}; - -export const useAllAttendances = () => { - return useQuery(ATTENDANCE_KEY, () => getAllAttendances().then((res) => res.data)); -}; - -export const useCheckChannelName = (teamId: string, enabled: boolean) => { - return useQuery<{ channelName: string | undefined; valid: boolean }, Error>( - ['attendance-check-channel', teamId], - () => getCheckChannelName(teamId).then((res) => res.data), - { enabled } - ); -}; - -export const useOngoingAttendance = (teamId: string) => { - return useQuery(['attendance-ongoing', teamId], () => - getOngoingAttendance(teamId).then((res) => res.data) - ); -}; - -export const useAttendanceById = (meetingAttendanceId: string, enabled: boolean) => { - return useQuery( - ['attendance', meetingAttendanceId], - () => getAttendanceById(meetingAttendanceId).then((res) => res.data), - { enabled } - ); -}; - -export const useCloseAttendance = () => { - const queryClient = useQueryClient(); - return useMutation((teamId) => postCloseAttendance(teamId).then(() => undefined), { - onSuccess: (_, teamId) => { - queryClient.invalidateQueries(ATTENDANCE_KEY); - queryClient.invalidateQueries(['attendance-ongoing', teamId]); - } - }); -}; diff --git a/src/frontend/src/hooks/bom.hooks.ts b/src/frontend/src/hooks/bom.hooks.ts index b078517aa3..85539f970e 100644 --- a/src/frontend/src/hooks/bom.hooks.ts +++ b/src/frontend/src/hooks/bom.hooks.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { Assembly, Manufacturer, Material, MaterialType, ProjectPreview, Unit, WbsNumber, wbsPipe } from 'shared'; +import { Assembly, Manufacturer, Material, MaterialType, Unit, WbsNumber, wbsPipe } from 'shared'; import { useToast } from '../hooks/toasts.hooks'; import { assignMaterialToAssembly, @@ -326,33 +326,3 @@ export const useGetMaterialsForWbsElement = (wbsNum: WbsNumber) => { return data; }); }; - -export const useGetMaterialsForCar = (carNumber: number | null, projects: ProjectPreview[]) => { - const projectsInCar = projects.filter((p) => p.wbsNum.carNumber === carNumber); - - return useQuery( - ['materials', 'car', carNumber ?? 'none'], - async () => { - const results = await Promise.all( - projectsInCar.map(async (p) => { - const { data } = await getMaterialsForWbsElement({ - carNumber: p.wbsNum.carNumber, - projectNumber: p.wbsNum.projectNumber, - workPackageNumber: 0 - }); - return data; - }) - ); - - const flat = results.flat(); - const seen = new Set(); - return flat.filter((material) => { - const key = `${material.name.toLowerCase()}-${material.assemblyId ?? 'no-assembly'}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - }, - { enabled: carNumber !== null && projectsInCar.length > 0 } - ); -}; diff --git a/src/frontend/src/hooks/calendar.hooks.ts b/src/frontend/src/hooks/calendar.hooks.ts index 98531729c5..0982bd658f 100644 --- a/src/frontend/src/hooks/calendar.hooks.ts +++ b/src/frontend/src/hooks/calendar.hooks.ts @@ -11,8 +11,7 @@ import { FilterArgs, ScheduleSlotCreateArgs, EventWithMembers, - ScheduleSlot, - EventInstance + ScheduleSlot } from 'shared'; import { getAllShops, @@ -49,8 +48,7 @@ import { getSingleEventWithMembers, previewScheduleSlotRecurringEdits, postDeleteScheduleSlot, - scheduleEvent, - getAllEventsPaginated + scheduleEvent } from '../apis/calendar.api'; import { useCurrentUser } from './users.hooks'; import { PDFDocument } from 'pdf-lib'; @@ -666,16 +664,3 @@ export const combinePdfsAndDownload = async (blobData: Blob[], filename: string) const pdfBlob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); saveAs(pdfBlob, filename); }; - -/** - * Custom hook to get all events in a paginated manner, sorted by scheduled date ascending. - */ -export const useAllEventsPaginated = (cursor?: Date, pageSize?: number) => { - return useQuery<{ instances: EventInstance[]; nextCursor: Date | null }, Error>( - ['events', 'paginated', cursor, pageSize], - async () => { - const { data } = await getAllEventsPaginated(cursor, pageSize); - return data; - } - ); -}; diff --git a/src/frontend/src/hooks/change-requests.hooks.ts b/src/frontend/src/hooks/change-requests.hooks.ts index 8f45ad0c7d..84061fac51 100644 --- a/src/frontend/src/hooks/change-requests.hooks.ts +++ b/src/frontend/src/hooks/change-requests.hooks.ts @@ -4,7 +4,6 @@ */ import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; import { ChangeRequest, ChangeRequestReason, @@ -13,8 +12,7 @@ import { ProposedSolutionCreateArgs, WbsNumber, WorkPackageProposedChangesCreateArgs, - LeadershipChangeCreateArgs, - GuestChangeRequest + LeadershipChangeCreateArgs } from 'shared'; import { createActivationChangeRequest, @@ -30,62 +28,38 @@ import { getUnreviewedChangeRequests, getApprovedChangeRequests, createBudgetChangeRequest, - createLeadershipChangeRequest, - getAllGuestChangeRequests + createLeadershipChangeRequest } from '../apis/change-requests.api'; /** * Custom React Hook to supply all change requests. */ export const useAllChangeRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['change requests', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getAllChangeRequests(); - return data; - } - ); -}; - -export const useAllGuestChangeRequests = () => { - return useQuery(['guest change requests'], async () => { - const { data } = await getAllGuestChangeRequests(); + return useQuery(['change requests'], async () => { + const { data } = await getAllChangeRequests(); return data; }); }; export const useGetToReviewChangeRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['change requests', 'to-review', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getToReviewChangeRequests(); - return data; - } - ); + return useQuery(['change requests', 'to-review'], async () => { + const { data } = await getToReviewChangeRequests(); + return data; + }); }; export const useGetUnreviewedChangeRequests = (wbsNum?: WbsNumber) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['change requests', 'unreviewed', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id, wbsNum], - async () => { - const { data } = await getUnreviewedChangeRequests(wbsNum); - return data; - } - ); + return useQuery(['change requests', 'unreviewed'], async () => { + const { data } = await getUnreviewedChangeRequests(wbsNum); + return data; + }); }; export const useGetApprovedChangeRequests = (wbsNum?: WbsNumber) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['change requests', 'approved', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id, wbsNum], - async () => { - const { data } = await getApprovedChangeRequests(wbsNum); - return data; - } - ); + return useQuery(['change requests', 'approved'], async () => { + const { data } = await getApprovedChangeRequests(wbsNum); + return data; + }); }; /** diff --git a/src/frontend/src/hooks/finance-car-filter.hooks.ts b/src/frontend/src/hooks/finance-car-filter.hooks.ts deleted file mode 100644 index 7cd034cedc..0000000000 --- a/src/frontend/src/hooks/finance-car-filter.hooks.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { useEffect, useState } from 'react'; -import { Car } from 'shared'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; - -export interface FinanceDashboardCarFilter { - selectedCar: Car | 'all-cars'; - allCars: Car[]; - startDate: Date | undefined; - endDate: Date | undefined; - setSelectedCar: (car: Car | 'all-cars') => void; - clearLocalSelection: () => void; - setStartDate: (date: Date | undefined) => void; - setEndDate: (date: Date | undefined) => void; - isLoading: boolean; - error: Error | null; -} - -/** - * Hook for Finance Dashboard car filtering with automatic date population. - * Uses local state only; does not mutate the global car selection. - * - * selectedCar is the resolved car (local override if set, otherwise global) and can be used - * directly as overrideCarId using selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id, - * keeping query keys reactive to both. - * - * When a specific car is selected, dates auto-populate: - * - Start date: When the car was initialized (car.dateCreated) - * - End date: Today (if current car) or start date of the next car (if previous car) - */ -export const useFinanceDashboardCarFilter = (initialStartDate?: Date, initialEndDate?: Date): FinanceDashboardCarFilter => { - const { selectedCar: globalSelectedCar, allCars, isLoading, error } = useGlobalCarFilter(); - - // undefined = not set (mirror global), 'all-cars' = explicitly set to "All Cars", Car = explicitly set to specific car - const [localSelectedCar, setLocalSelectedCar] = useState(undefined); - const [startDate, setStartDate] = useState(initialStartDate); - const [endDate, setEndDate] = useState(initialEndDate); - - const setSelectedCar = (car: Car | 'all-cars') => { - setLocalSelectedCar(car); - }; - - const clearLocalSelection = () => { - setLocalSelectedCar(undefined); - }; - - // Resolved car: local override if set, otherwise mirrors the global car. - const selectedCar = localSelectedCar !== undefined ? localSelectedCar : globalSelectedCar; - - // Auto-populate dates from the resolved car. - useEffect(() => { - if (selectedCar === 'all-cars') { - setStartDate(undefined); - setEndDate(undefined); - } else if (allCars.length > 0) { - setStartDate(new Date(selectedCar.dateCreated)); - const isCurrentCar = isCarCurrent(selectedCar, allCars); - if (isCurrentCar) { - setEndDate(new Date()); - } else { - const nextCar = findNextCar(selectedCar, allCars); - setEndDate(nextCar ? new Date(nextCar.dateCreated) : new Date()); - } - } - }, [selectedCar, allCars]); - - return { - selectedCar, - allCars, - startDate, - endDate, - setSelectedCar, - clearLocalSelection, - setStartDate, - setEndDate, - isLoading, - error - }; -}; - -/** - * Determines if the given car is the current/most recent car - */ -const isCarCurrent = (car: Car, allCars: Car[]): boolean => { - const maxCarNumber = Math.max(...allCars.map((c) => c.wbsNum.carNumber)); - return car.wbsNum.carNumber === maxCarNumber; -}; - -/** - * Finds the next car in chronological order (by car number) - */ -const findNextCar = (car: Car, allCars: Car[]): Car | null => { - const sortedCars = allCars - .filter((c) => c.wbsNum.carNumber > car.wbsNum.carNumber) - .sort((a, b) => a.wbsNum.carNumber - b.wbsNum.carNumber); - - return sortedCars[0] || null; -}; diff --git a/src/frontend/src/hooks/finance.hooks.ts b/src/frontend/src/hooks/finance.hooks.ts index 0a225f2061..6c7baacc79 100644 --- a/src/frontend/src/hooks/finance.hooks.ts +++ b/src/frontend/src/hooks/finance.hooks.ts @@ -102,7 +102,6 @@ import { ProspectiveSponsor } from 'shared'; import { fullNamePipe } from '../utils/pipes'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; /** * Helper function to handle file upload errors with file name context @@ -327,47 +326,47 @@ export interface ReimbursementRequestTeamDataPayload { teamId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface ReimbursementRequestDataPayload { startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface ReimbursementRequestCategoryDataPayload { otherReasonId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface ReimbursementRequestTeamTypeDataPayload { teamTypeId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface SpendingBarTeamDataPayload { teamId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface SpendingBarTeamTypeDataPayload { teamTypeId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } export interface SpendingBarDataPayload { startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } /** @@ -466,42 +465,30 @@ export const useGetAllAccountCodes = () => { * Custom React Hook to get the reimbursement requests created by the current user */ export const useCurrentUserReimbursementRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['reimbursement-requests', 'user', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getCurrentUserReimbursementRequests(); - return data; - } - ); + return useQuery(['reimbursement-requests', 'user'], async () => { + const { data } = await getCurrentUserReimbursementRequests(); + return data; + }); }; /** * Custom React Hook to get the reimbursement requests assigned to the current user */ export const useCurrentUserAssignedReimbursementRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['reimbursement-requests', 'assignee', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getCurrentUserAssignedReimbursementRequests(); - return data; - } - ); + return useQuery(['reimbursement-requests', 'assignee'], async () => { + const { data } = await getCurrentUserAssignedReimbursementRequests(); + return data; + }); }; /** * Custom React Hook to get the reimbursement requests for the current user's teams */ export const useCurrentUsersTeamsReimbursementRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['reimbursement-requests', 'teams', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getCurrentUsersTeamsReimbursementRequests(); - return data; - } - ); + return useQuery(['reimbursement-requests', 'user'], async () => { + const { data } = await getCurrentUsersTeamsReimbursementRequests(); + return data; + }); }; /** @@ -550,14 +537,10 @@ export const useSetTaxExemptStatus = () => { * Custom React Hook to get all the reimbursement requests */ export const useAllReimbursementRequests = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['reimbursement-requests', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getAllReimbursementRequests(); - return data; - } - ); + return useQuery(['reimbursement-requests'], async () => { + const { data } = await getAllReimbursementRequests(); + return data; + }); }; /** @@ -799,14 +782,10 @@ export const useDownloadCSVFileOfReimbursementRequests = () => { * @returns the list of Reimbursement Reqeusts that are pending Advisor Approval */ export const useGetPendingAdvisorList = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['reimbursement-requests', 'pending-advisors', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getPendingAdvisorList(); - return data; - } - ); + return useQuery(['reimbursement-requests', 'pending-advisors'], async () => { + const { data } = await getPendingAdvisorList(); + return data; + }); }; /** @@ -1181,7 +1160,7 @@ export const useGetReimbursementRequestTeamData = (reimbursementRequestData: Rei 'reimbursement-request-team-data', reimbursementRequestData.endDate, reimbursementRequestData.startDate, - reimbursementRequestData.overrideCarId, + reimbursementRequestData.carNumber, reimbursementRequestData.teamId ], async () => { @@ -1196,7 +1175,7 @@ export const useGetReimbursementRequestTeamTypeData = (reimbursementRequestData: 'reimbursement-request-team-type-data', reimbursementRequestData.endDate, reimbursementRequestData.startDate, - reimbursementRequestData.overrideCarId, + reimbursementRequestData.carNumber, reimbursementRequestData.teamTypeId ], async () => { @@ -1225,7 +1204,7 @@ export const useGetReimbursementRequestCategoryData = (reimbursementRequestData: 'reimbursement-request-category-data', reimbursementRequestData.endDate, reimbursementRequestData.startDate, - reimbursementRequestData.overrideCarId, + reimbursementRequestData.carNumber, reimbursementRequestData.otherReasonId ], async () => { @@ -1240,7 +1219,7 @@ export const useGetAllReimbursementRequestData = (reimbursementRequestData: Reim 'reimbursement-request-data', reimbursementRequestData.endDate, reimbursementRequestData.startDate, - reimbursementRequestData.overrideCarId + reimbursementRequestData.carNumber ], async () => { const { data } = await getAllReimbursementRequestData(reimbursementRequestData); @@ -1254,7 +1233,7 @@ export const useGetSpendingBarTeamData = (spendingBarData: SpendingBarTeamDataPa 'spending-bar-team-data', spendingBarData.endDate, spendingBarData.startDate, - spendingBarData.overrideCarId, + spendingBarData.carNumber, spendingBarData.teamId ], async () => { @@ -1269,7 +1248,7 @@ export const useGetSpendingBarTeamTypeData = (spendingBarData: SpendingBarTeamTy 'spending-bar-team-type-data', spendingBarData.endDate, spendingBarData.startDate, - spendingBarData.overrideCarId, + spendingBarData.carNumber, spendingBarData.teamTypeId ], async () => { @@ -1280,7 +1259,7 @@ export const useGetSpendingBarTeamTypeData = (spendingBarData: SpendingBarTeamTy export const useGetSpendingBarCategoryData = (spendingBarData: SpendingBarDataPayload) => useQuery( - ['spending-bar-category-data', spendingBarData.endDate, spendingBarData.startDate, spendingBarData.overrideCarId], + ['spending-bar-category-data', spendingBarData.endDate, spendingBarData.startDate, spendingBarData.carNumber], async () => { const { data } = await getSpendingBarCategoryData(spendingBarData); return data; @@ -1289,7 +1268,7 @@ export const useGetSpendingBarCategoryData = (spendingBarData: SpendingBarDataPa export const useGetAllSpendingBarData = (spendingBarData: SpendingBarDataPayload) => useQuery( - ['spending-bar-data', spendingBarData.endDate, spendingBarData.startDate, spendingBarData.overrideCarId], + ['spending-bar-data', spendingBarData.endDate, spendingBarData.startDate, spendingBarData.carNumber], async () => { const { data } = await getAllSpendingBarData(spendingBarData); return data; diff --git a/src/frontend/src/hooks/projects.hooks.ts b/src/frontend/src/hooks/projects.hooks.ts index baccb799cb..a95e17313c 100644 --- a/src/frontend/src/hooks/projects.hooks.ts +++ b/src/frontend/src/hooks/projects.hooks.ts @@ -39,13 +39,12 @@ import { } from '../apis/projects.api'; import { CreateSingleProjectPayload, EditSingleProjectPayload } from '../utils/types'; import { useCurrentUser } from './users.hooks'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; /** * Custom React Hook to supply all projects with Gantt querry args */ export const useAllProjectsGantt = () => { - return useQuery(['projects', 'gantt-all'], async () => { + return useQuery(['projects'], async () => { const { data } = await getAllProjectsGantt(); return data; }); @@ -55,56 +54,40 @@ export const useAllProjectsGantt = () => { * Custom React Hook to supply all projects */ export const useAllProjects = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['projects', 'previews', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getAllProjects(); - return data; - } - ); + return useQuery(['projects', 'previews'], async () => { + const { data } = await getAllProjects(); + return data; + }); }; /** * Custom React Hook to supply all of the projects that are on the users teams */ export const useGetUsersTeamsProjects = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['projects', 'teams', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getUsersTeamsProjects(); - return data; - } - ); + return useQuery(['projects', 'teams'], async () => { + const { data } = await getUsersTeamsProjects(); + return data; + }); }; /** * Custom React Hook to supply all of the projects that the user is the manager or lead of */ export const useGetUsersLeadingProjects = () => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['projects', 'leading', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getUsersLeadingProjects(); - return data; - } - ); + return useQuery(['projects', 'leading'], async () => { + const { data } = await getUsersLeadingProjects(); + return data; + }); }; /** * Custom React Hook to supply all of the projects for a given team */ export const useGetTeamsProjects = (teamId: string) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['projects', 'teams', teamId, selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getTeamsProjects(teamId); - return data; - } - ); + return useQuery(['projects', 'teams'], async () => { + const { data } = await getTeamsProjects(teamId); + return data; + }); }; /** diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index c890c23671..a7b18e9a4f 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -42,7 +42,6 @@ import { import { useAuth } from './auth.hooks'; import { useContext } from 'react'; import { UserContext } from '../app/AppContextUser'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; /** * Custom React Hook to supply the current user @@ -189,14 +188,10 @@ export const useUserScheduleSettings = (id: string) => { * @param id User ID of the requested user's settings. */ export const useUsersFavoriteProjects = (id: string) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['users', id, 'favorite projects', selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getUsersFavoriteProjects(id); - return data; - } - ); + return useQuery(['users', id, 'favorite projects'], async () => { + const { data } = await getUsersFavoriteProjects(id); + return data; + }); }; /** diff --git a/src/frontend/src/hooks/work-packages.hooks.ts b/src/frontend/src/hooks/work-packages.hooks.ts index 4c34ebc0b2..acf0fa7d92 100644 --- a/src/frontend/src/hooks/work-packages.hooks.ts +++ b/src/frontend/src/hooks/work-packages.hooks.ts @@ -19,34 +19,25 @@ import { WorkPackageEditArgs, getHomePageWorkPackages } from '../apis/work-packages.api'; -import { useGlobalCarFilter } from '../app/AppGlobalCarFilterContext'; /** * Custom React Hook to supply all work packages. */ export const useAllWorkPackages = (queryParams?: { [field: string]: string }) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['work packages', queryParams, selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getAllWorkPackages(queryParams); - return data; - } - ); + return useQuery(['work packages', queryParams], async () => { + const { data } = await getAllWorkPackages(queryParams); + return data; + }); }; /** * Custom React Hook to supply all work packages in preview format (minimal data). */ export const useAllWorkPackagesPreview = (status?: string) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['work packages', 'preview', status, selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getAllWorkPackagesPreview(status); - return data; - } - ); + return useQuery(['work packages', 'preview', status], async () => { + const { data } = await getAllWorkPackagesPreview(status); + return data; + }); }; /** @@ -137,12 +128,8 @@ export const useGetBlockingWorkPackages = (wbsNum: WbsNumber) => { * Custom React Hook to get many work packages */ export const useGetManyWorkPackages = (wbsNums: WbsNumber[]) => { - const { selectedCar } = useGlobalCarFilter(); - const filteredWbsNums = - selectedCar === 'all-cars' ? wbsNums : wbsNums.filter((wbsNum) => wbsNum.carNumber === selectedCar.wbsNum.carNumber); - const carKey = selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id; - return useQuery(['work packages', 'many', filteredWbsNums, carKey], async () => { - const { data } = await getManyWorkPackages(filteredWbsNums); + return useQuery(['work packages', 'blocking', wbsNums], async () => { + const { data } = await getManyWorkPackages(wbsNums); return data; }); }; @@ -158,12 +145,8 @@ export const useSlackUpcomingDeadlines = () => { }; export const useHomeScreenWorkPackages = (selection: WorkPackageSelection) => { - const { selectedCar } = useGlobalCarFilter(); - return useQuery( - ['teams', 'work-packages', selection, selectedCar === 'all-cars' ? 'all-cars' : selectedCar.id], - async () => { - const { data } = await getHomePageWorkPackages(selection); - return data; - } - ); + return useQuery(['teams', 'work-packages', selection], async () => { + const { data } = await getHomePageWorkPackages(selection); + return data; + }); }; diff --git a/src/frontend/src/layouts/Sidebar/NavPageLink.tsx b/src/frontend/src/layouts/Sidebar/NavPageLink.tsx index 6dcd5cbe8e..9fe391bb53 100644 --- a/src/frontend/src/layouts/Sidebar/NavPageLink.tsx +++ b/src/frontend/src/layouts/Sidebar/NavPageLink.tsx @@ -94,7 +94,7 @@ const NavPageLink: React.FC = ({ {subItems && ( {subItems.map((subItem) => ( - + ))} )} diff --git a/src/frontend/src/layouts/Sidebar/Sidebar.tsx b/src/frontend/src/layouts/Sidebar/Sidebar.tsx index e953360a33..93e731e345 100644 --- a/src/frontend/src/layouts/Sidebar/Sidebar.tsx +++ b/src/frontend/src/layouts/Sidebar/Sidebar.tsx @@ -9,6 +9,7 @@ import styles from '../../stylesheets/layouts/sidebar/sidebar.module.css'; import { Typography, Box, IconButton, Divider } from '@mui/material'; import HomeIcon from '@mui/icons-material/Home'; import AlignHorizontalLeftIcon from '@mui/icons-material/AlignHorizontalLeft'; +import RateReviewIcon from '@mui/icons-material/RateReview'; import DashboardIcon from '@mui/icons-material/Dashboard'; // To be uncommented after guest sponsors page is developed // import VolunteerActivismIcon from '@mui/icons-material/VolunteerActivism'; @@ -27,19 +28,15 @@ import { useHomePageContext } from '../../app/HomePageContext'; // once divisions developed, import TeamType from shared import { isGuest } from 'shared'; // To be uncommented after divisions page is developed -import * as MuiIcons from '@mui/icons-material'; -import { useAllTeamTypes } from '../../hooks/team-types.hooks'; -import { TeamType } from 'shared'; -import ErrorPage from '../../pages/ErrorPage'; +// import * as MuiIcons from '@mui/icons-material'; +// import { useAllTeamTypes } from '../../hooks/team-types.hooks'; +// import ErrorPage from '../../pages/ErrorPage'; import BarChartIcon from '@mui/icons-material/BarChart'; import { useCurrentUser } from '../../hooks/users.hooks'; import QueryStatsIcon from '@mui/icons-material/QueryStats'; import CurrencyExchangeIcon from '@mui/icons-material/CurrencyExchange'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { useState } from 'react'; -import GlobalCarFilterHeader from '../../components/GlobalCarFilterHeader'; -import GlobalCarFilterChips from '../../components/GlobalCarFilterChips'; -import { CalendarIcon } from '@mui/x-date-pickers'; interface SidebarProps { drawerOpen: boolean; @@ -53,18 +50,19 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid const { onPNMHomePage, onOnboardingHomePage } = useHomePageContext(); const user = useCurrentUser(); const { onGuestHomePage } = useHomePageContext(); - const { isError: teamsError, error: teamsErrorMsg, data: teams } = useAllTeamTypes(); + // const { isError: teamsError, error: teamsErrorMsg, data: teams } = useAllTeamTypes(); - const allTeams: LinkItem[] = (teams ?? []).map((team: TeamType) => { - const IconComponent = MuiIcons[(team.iconName in MuiIcons ? team.iconName : 'Circle') as keyof typeof MuiIcons]; - return { - name: team.name, - icon: , - route: routes.TEAMS + '/' + team.teamTypeId - }; - }); + // To be uncommented once guest divisions pages are developed + // const allTeams: LinkItem[] = (teams ?? []).map((team: TeamType) => { + // const IconComponent = MuiIcons[(team.iconName in MuiIcons ? team.iconName : 'Circle') as keyof typeof MuiIcons]; + // return { + // name: team.name, + // icon: , + // route: routes.TEAMS + '/' + team.teamTypeId + // }; + // }); - if (teamsError) return ; + // if (teamsError) return ; const memberLinkItems: LinkItem[] = [ { name: 'Home', @@ -103,9 +101,9 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid route: routes.CHANGE_REQUESTS }, { - name: 'Events', - icon: , - route: routes.EVENTS + name: 'Design Review', + icon: , + route: routes.CALENDAR } ] }, @@ -138,18 +136,23 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid }, // Teams tab here to be replaced with below code once guest divisions is developed - !onGuestHomePage - ? { - name: 'Teams', - icon: , - route: routes.TEAMS - } - : { - name: 'Divisions', - icon: , - route: routes.TEAMS, - subItems: allTeams - }, + !onGuestHomePage && { + name: 'Teams', + icon: , + route: routes.TEAMS + }, + // !onGuestHomePage + // ? { + // name: 'Teams', + // icon: , + // route: routes.TEAMS + // } + // : { + // name: 'Divisions', + // icon: , + // route: routes.TEAMS, + // subItems: allTeams + // }, !onGuestHomePage && { name: 'Calendar', icon: , @@ -220,15 +223,7 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid }} > - - - - handleMoveContent()} sx={{ p: 0.5 }}> - {moveContent ? : } - - - - + handleMoveContent()}>{moveContent ? : } {linkItems.map((linkItem) => ( handleOpenSubmenu(linkItem.name)} onSubmenuCollapse={() => handleCloseSubmenu()} /> ))} - diff --git a/src/frontend/src/layouts/SidebarLayout.tsx b/src/frontend/src/layouts/SidebarLayout.tsx deleted file mode 100644 index 5d3a865dff..0000000000 --- a/src/frontend/src/layouts/SidebarLayout.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { Box } from '@mui/system'; -import { Container, IconButton, useTheme } from '@mui/material'; -import { useState } from 'react'; -import ArrowCircleRightTwoToneIcon from '@mui/icons-material/ArrowCircleRightTwoTone'; -import Sidebar from './Sidebar/Sidebar'; -import HiddenContentMargin from '../components/HiddenContentMargin'; -import { useHomePageContext } from '../app/HomePageContext'; - -const SidebarLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const theme = useTheme(); - const [drawerOpen, setDrawerOpen] = useState(false); - const [moveContent, setMoveContent] = useState(false); - const { onGuestHomePage } = useHomePageContext(); - - return ( - <> - { - setDrawerOpen(true); - }} - sx={{ - height: '100vh', - position: 'fixed', - width: 15, - borderRight: 2, - borderRightColor: theme.palette.background.paper - }} - /> - { - setDrawerOpen(true); - setMoveContent(true); - }} - sx={{ position: 'fixed', left: -8, top: '3%' }} - id="sidebar-button" - > - - - - - - - {children} - - - - ); -}; - -export default SidebarLayout; diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsAttendanceConfig.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsAttendanceConfig.tsx deleted file mode 100644 index 90315139e6..0000000000 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsAttendanceConfig.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useState } from 'react'; -import { - Box, - Collapse, - IconButton, - Paper, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography -} from '@mui/material'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; -import { useAllAttendances, useAttendanceById } from '../../hooks/attendance.hooks'; -import { fullNamePipe } from '../../utils/pipes'; -import { MeetingAttendance } from 'shared'; - -interface AttendanceRowProps { - attendance: MeetingAttendance; -} - -const AttendanceRow: React.FC = ({ attendance }) => { - const [open, setOpen] = useState(false); - const { data: attendanceWithAttendees, isLoading } = useAttendanceById(attendance.meetingAttendanceId, open); - - return ( - <> - setOpen((prev) => !prev)}> - - { - e.stopPropagation(); - setOpen((prev) => !prev); - }} - > - {open ? : } - - - {attendance.teamName} - {fullNamePipe(attendance.userCreated)} - {new Date(attendance.openedAt).toLocaleString()} - {attendance.attendeesCount} - {attendance.teamMemberAttendancePercent.toFixed(1)}% - - - - - - - Attendees - - {isLoading ? ( - - ) : !attendanceWithAttendees || attendanceWithAttendees.attendees.length === 0 ? ( - - No attendees recorded. - - ) : ( - - {attendanceWithAttendees.attendees.map((user) => ( - - {fullNamePipe(user)} - - ))} - - )} - - - - - - ); -}; - -const AdminToolsAttendanceConfig: React.FC = () => { - const { data: attendances, isLoading, isError, error } = useAllAttendances(); - - if (isLoading) return ; - if (isError) return ; - - return ( - - - Attendance - - - - - - - Team Name - Initiated By - Date/Time - Attendees - % of Team - - - - {!attendances || attendances.length === 0 ? ( - - - No attendance records yet. - - - ) : ( - attendances.map((attendance) => ) - )} - -
-
-
- ); -}; - -export default AdminToolsAttendanceConfig; diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx index bd58fb9101..03a52f9e2f 100644 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx @@ -22,7 +22,6 @@ import GuestViewConfig from './EditGuestView/GuestViewConfig'; import AdminToolsSlackIds from './AdminToolsSlackIds'; import AdminToolsOnboardingConfig from './OnboardingConfig/AdminToolsOnboardingConfig'; import AdminToolsScheduleConfig from './ScheduleConfig/AdminToolsScheduleConfig'; -import AdminToolsAttendanceConfig from './AdminToolsAttendanceConfig'; const AdminToolsPage: React.FC = () => { const currentUser = useCurrentUser(); @@ -48,7 +47,6 @@ const AdminToolsPage: React.FC = () => { tabs.push({ tabUrlValue: 'recruitment', tabName: 'Recruitment' }); tabs.push({ tabUrlValue: 'guest-view', tabName: 'Guest View' }); tabs.push({ tabUrlValue: 'onboarding', tabName: 'Onboarding' }); - tabs.push({ tabUrlValue: 'attendance', tabName: 'Attendance' }); tabs.push({ tabUrlValue: 'miscellaneous', tabName: 'Miscellaneous' }); } @@ -59,7 +57,6 @@ const AdminToolsPage: React.FC = () => { { ) : tabIndex === 6 ? ( - ) : tabIndex === 7 ? ( - ) : ( diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx index 490d595cfa..d4c10f0e76 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx @@ -67,7 +67,7 @@ const EditLogo = () => { ) : ( <> diff --git a/src/frontend/src/pages/AdminToolsPage/ScheduleConfig/EventType/EventTypeFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/ScheduleConfig/EventType/EventTypeFormModal.tsx index 8593664d2f..6f952c6985 100644 --- a/src/frontend/src/pages/AdminToolsPage/ScheduleConfig/EventType/EventTypeFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ScheduleConfig/EventType/EventTypeFormModal.tsx @@ -1,11 +1,11 @@ -import { Box, FormHelperText, Typography, Checkbox, FormControl, Select, MenuItem, Tooltip } from '@mui/material'; +import { Box, FormHelperText, Typography, Checkbox, FormControl, Select, MenuItem } from '@mui/material'; import NERFormModal from '../../../../components/NERFormModal'; import ReactHookTextField from '../../../../components/ReactHookTextField'; import { useToast } from '../../../../hooks/toasts.hooks'; import { useForm, Controller } from 'react-hook-form'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; -import React, { useEffect } from 'react'; +import React from 'react'; import { EventType } from 'shared'; import useFormPersist from 'react-hook-form-persist'; import { FormStorageKey } from '../../../../utils/form'; @@ -116,16 +116,6 @@ export const EventTypeFormModal: React.FC = ({ open, on setValue }); - const watchTeams = watch('teams'); - const watchWorkPackage = watch('workPackage'); - const notificationsDisabled = !watchTeams && !watchWorkPackage; - - useEffect(() => { - if (notificationsDisabled) { - setValue('sendSlackNotifications', false); - } - }, [notificationsDisabled, setValue]); - const onFormSubmit = async (data: EventTypeFormValues) => { try { await onSubmit(data); @@ -839,22 +829,12 @@ export const EventTypeFormModal: React.FC = ({ open, on control={control} name="sendSlackNotifications" render={({ field: { onChange, value } }) => ( - + )} /> - - - + - + Send Slack Notifications diff --git a/src/frontend/src/pages/CalendarPage/CalendarDayCard.tsx b/src/frontend/src/pages/CalendarPage/CalendarDayCard.tsx index 9d2a12ee5c..e0b30f2cfd 100644 --- a/src/frontend/src/pages/CalendarPage/CalendarDayCard.tsx +++ b/src/frontend/src/pages/CalendarPage/CalendarDayCard.tsx @@ -168,10 +168,6 @@ const CalendarDayCard: React.FC = ({ const [tooltipHovered, setTooltipHovered] = useState(false); const tooltipKey = `task-${task.taskId}`; const isLocked = lockedTooltipEventId === tooltipKey; - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const shouldBeOpen = isLocked || isHovered || tooltipHovered; return ( @@ -181,9 +177,8 @@ const CalendarDayCard: React.FC = ({ marginRight={0.5} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) { + if (!isLocked && !tooltipHovered) { setIsHovered(false); } }, 100); @@ -304,10 +299,6 @@ const CalendarDayCard: React.FC = ({ event.approved === ConflictStatus.DENIED; const bgColor = isPending ? getMutedColor(baseColor, 0.35) : baseColor; const isLocked = lockedTooltipEventId === event.eventId; - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const shouldBeOpen = isLocked || isHovered || tooltipHovered; return ( @@ -317,9 +308,8 @@ const CalendarDayCard: React.FC = ({ marginRight={0.5} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) { + if (!isLocked && !tooltipHovered) { setIsHovered(false); } }, 100); @@ -424,10 +414,6 @@ const CalendarDayCard: React.FC = ({ const [isHovered, setIsHovered] = useState(false); const [tooltipHovered, setTooltipHovered] = useState(false); const isLocked = lockedTooltipEventId === event.eventId; - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const shouldBeOpen = isLocked || isHovered || tooltipHovered; return ( @@ -489,9 +475,8 @@ const CalendarDayCard: React.FC = ({ setIsHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) { + if (!isLocked && !tooltipHovered) { setIsHovered(false); } }, 100); @@ -512,10 +497,6 @@ const CalendarDayCard: React.FC = ({ const [tooltipHovered, setTooltipHovered] = useState(false); const tooltipKey = `task-${task.taskId}`; const isLocked = lockedTooltipEventId === tooltipKey; - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const shouldBeOpen = isLocked || isHovered || tooltipHovered; return ( @@ -566,9 +547,8 @@ const CalendarDayCard: React.FC = ({ setIsHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) { + if (!isLocked && !tooltipHovered) { setIsHovered(false); } }, 100); diff --git a/src/frontend/src/pages/CalendarPage/CalendarWeekView.tsx b/src/frontend/src/pages/CalendarPage/CalendarWeekView.tsx index b0289549b7..dcccbc2085 100644 --- a/src/frontend/src/pages/CalendarPage/CalendarWeekView.tsx +++ b/src/frontend/src/pages/CalendarPage/CalendarWeekView.tsx @@ -379,11 +379,7 @@ const CalendarWeekView: React.FC = ({ const WeekEventBlock = ({ event, layout }: { event: EventInstance; layout: LayoutEvent }) => { const [blockHovered, setBlockHovered] = useState(false); const [tooltipHovered, setTooltipHovered] = useState(false); - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; const isLocked = lockedTooltipEventId === event.eventId + event.scheduleSlotId; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const isOpen = isLocked || blockHovered || tooltipHovered; const baseColor = getEventColor(event); @@ -409,11 +405,7 @@ const CalendarWeekView: React.FC = ({ enterDelay={0} leaveDelay={200} title={ - setTooltipHovered(true)} - onMouseLeave={() => setTooltipHovered(false)} - onMouseDown={(e) => e.stopPropagation()} - > + setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}> = ({ data-testid="week-event-block" onMouseEnter={() => setBlockHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false); + if (!isLocked && !tooltipHovered) setBlockHovered(false); }, 100); }} onClick={(e) => { @@ -500,11 +491,7 @@ const CalendarWeekView: React.FC = ({ const AllDayEventBlock = ({ event }: { event: EventInstance }) => { const [blockHovered, setBlockHovered] = useState(false); const [tooltipHovered, setTooltipHovered] = useState(false); - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; const isLocked = lockedTooltipEventId === event.eventId + event.scheduleSlotId; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const isOpen = isLocked || blockHovered || tooltipHovered; const baseColor = getEventColor(event); @@ -526,11 +513,7 @@ const CalendarWeekView: React.FC = ({ enterDelay={0} leaveDelay={200} title={ - setTooltipHovered(true)} - onMouseLeave={() => setTooltipHovered(false)} - onMouseDown={(e) => e.stopPropagation()} - > + setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}> = ({ setBlockHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false); + if (!isLocked && !tooltipHovered) setBlockHovered(false); }, 100); }} onClick={(e) => { @@ -593,12 +575,8 @@ const CalendarWeekView: React.FC = ({ const AllDayTaskBlock = ({ task }: { task: CalendarTask }) => { const [blockHovered, setBlockHovered] = useState(false); const [tooltipHovered, setTooltipHovered] = useState(false); - const tooltipHoveredRef = useRef(false); - tooltipHoveredRef.current = tooltipHovered; const tooltipKey = `task-${task.taskId}`; const isLocked = lockedTooltipEventId === tooltipKey; - const isLockedRef = useRef(false); - isLockedRef.current = isLocked; const isOpen = isLocked || blockHovered || tooltipHovered; return ( @@ -612,11 +590,7 @@ const CalendarWeekView: React.FC = ({ enterDelay={0} leaveDelay={200} title={ - setTooltipHovered(true)} - onMouseLeave={() => setTooltipHovered(false)} - onMouseDown={(e) => e.stopPropagation()} - > + setTooltipHovered(true)} onMouseLeave={() => setTooltipHovered(false)}> setLockedTooltipEventId(null)} /> } @@ -638,9 +612,8 @@ const CalendarWeekView: React.FC = ({ setBlockHovered(true)} onMouseLeave={() => { - setTooltipHovered(false); setTimeout(() => { - if (!isLockedRef.current && !tooltipHoveredRef.current) setBlockHovered(false); + if (!isLocked && !tooltipHovered) setBlockHovered(false); }, 100); }} onClick={(e) => { diff --git a/src/frontend/src/pages/CalendarPage/Components/EventModal.tsx b/src/frontend/src/pages/CalendarPage/Components/EventModal.tsx index 18b2ceaece..b84d5360ad 100644 --- a/src/frontend/src/pages/CalendarPage/Components/EventModal.tsx +++ b/src/frontend/src/pages/CalendarPage/Components/EventModal.tsx @@ -31,7 +31,7 @@ import { getDay } from 'shared'; import { useToast } from '../../../hooks/toasts.hooks'; -import { useAllMembers, useCurrentUser } from '../../../hooks/users.hooks'; +import { useAllUsers, useCurrentUser } from '../../../hooks/users.hooks'; import { useAllWorkPackagesPreview } from '../../../hooks/work-packages.hooks'; import { useAllTeamPreviews } from '../../../hooks/teams.hooks'; import { userToAutocompleteOption } from '../../../utils/teams.utils'; @@ -54,8 +54,6 @@ import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import Tooltip from '@mui/material/Tooltip'; import { convertDayToInt, convertIntToDay } from '../../../utils/calendar.utils'; import EditSeriesConfirmationModal from './EditSeriesConfirmationModal'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; export interface EventFormValues { title: string; @@ -239,9 +237,6 @@ const EventModal: React.FC = ({ const [pendingPayload, setPendingPayload] = useState(null); const [pendingFormData, setPendingFormData] = useState(null); - // used in edit mode for ability to send notifs when wp changes - const [workPackageIds, setWorkPackageIds] = useState(initialValues?.workPackageIds ?? []); - // Fetch preview of other schedule slots that would be affected when editing with "edit all in series" const isEditMode = !!initialValues; const scheduleSlotId = initialValues?.selectedScheduleSlotId; @@ -252,7 +247,7 @@ const EventModal: React.FC = ({ ); // Lazy load all data needed for the form so users can start filling out instantly - const { isLoading: usersLoading, isError: usersError, error: usersErrorMsg, data: users } = useAllMembers(); + const { isLoading: usersLoading, isError: usersError, error: usersErrorMsg, data: users } = useAllUsers(); const { isLoading: shopsLoading, isError: shopsError, error: shopsErrorMsg, data: shops } = useAllShops(); const { isError: machineryError, error: machineryErrorMsg, data: machinery } = useAllMachines(); const { @@ -1156,43 +1151,6 @@ const EventModal: React.FC = ({ )} )} - {/* Notification Section */} - {selectedEventType && ( - - - - - - {selectedEventType.sendSlackNotifications ? 'On' : 'Off'} - - {selectedEventType.sendSlackNotifications && !selectedTeams.length && !workPackageIds.length && ( - - - - Add{' '} - {selectedEventType.teams && selectedEventType.workPackage - ? 'a team or work package' - : selectedEventType.teams - ? 'a team' - : 'a work package'}{' '} - to send notifications - - - )} - - - - )} {/* Required Members Section */} {selectedEventType?.requiredMembers && ( @@ -1459,9 +1417,7 @@ const EventModal: React.FC = ({ value={workPackageOptions.find((wp) => value?.[0] === wp.id) || null} onChange={(_, newValue) => { if (newValue?.id !== 'loading') { - const ids = newValue ? [newValue.id] : []; - onChange(ids); - setWorkPackageIds(ids); + onChange(newValue ? [newValue.id] : []); } }} getOptionLabel={(option) => option.label} diff --git a/src/frontend/src/pages/CalendarPage/EventClickPopup.tsx b/src/frontend/src/pages/CalendarPage/EventClickPopup.tsx index 2694f2fc13..bd7dfe3180 100644 --- a/src/frontend/src/pages/CalendarPage/EventClickPopup.tsx +++ b/src/frontend/src/pages/CalendarPage/EventClickPopup.tsx @@ -39,7 +39,7 @@ import EditEventModal from './Components/EditEventModal'; import DeleteSeriesConfirmationModal from './Components/DeleteSeriesConfirmationModal'; import { useToast } from '../../hooks/toasts.hooks'; import NERDeleteModal from '../../components/NERDeleteModal'; -import NotificationsIcon from '@mui/icons-material/Notifications'; + import { getPendingReason } from '../../utils/calendar.utils'; export const getStatusIcon = (status: string, isLarge?: boolean) => { @@ -436,9 +436,6 @@ export const EventClickContent: React.FC = ({ Status: {event.status} - {specificEventType?.sendSlackNotifications && (event.teams.length > 0 || event.workPackages.length > 0) && ( - - )} )} diff --git a/src/frontend/src/pages/CalendarPage/FilterModal.tsx b/src/frontend/src/pages/CalendarPage/FilterModal.tsx index 849d744ca4..0b6e88b474 100644 --- a/src/frontend/src/pages/CalendarPage/FilterModal.tsx +++ b/src/frontend/src/pages/CalendarPage/FilterModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Autocomplete, Box, Button, Checkbox, TextField, Typography } from '@mui/material'; import NERModal from '../../components/NERModal'; import PeopleIcon from '@mui/icons-material/People'; -import { useAllMembers, useCurrentUser } from '../../hooks/users.hooks'; +import { useAllUsers, useCurrentUser } from '../../hooks/users.hooks'; import { useAllTeams } from '../../hooks/teams.hooks'; import ErrorPage from '../ErrorPage'; @@ -39,7 +39,7 @@ const FilterModal: React.FC = ({ const MemberDropdown = () => { const memberIds = filterValues?.memberIds ?? []; - const { data: allUsers } = useAllMembers(); + const { data: allUsers } = useAllUsers(); return ( diff --git a/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx b/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx index 90b688626e..c6b22d0b05 100644 --- a/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx +++ b/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx @@ -5,24 +5,18 @@ import { routes } from '../../utils/routes'; import { isGuest } from 'shared'; import { Add } from '@mui/icons-material'; import { useCurrentUser } from '../../hooks/users.hooks'; -import { useGlobalCarFilter } from '../../app/AppGlobalCarFilterContext'; import ChangeRequestsOverview from './ChangeRequestsOverview'; import ChangeRequestsTable from './ChangeRequestsTable'; import PageLayout from '../../components/PageLayout'; import FullPageTabs from '../../components/FullPageTabs'; -import GuestChangeRequestsPage from './GuestChangeRequestsPage'; const ChangeRequestsView: React.FC = () => { const history = useHistory(); const user = useCurrentUser(); - const { selectedCar } = useGlobalCarFilter(); // Default to the "overview" tab const [tabIndex, setTabIndex] = useState(0); - if (isGuest(user.role)) { - return ; - } const headerRight = ( { return ( { - const theme = useTheme(); - - const submitterName = - cr.submitter?.firstName && cr.submitter?.lastName ? `${cr.submitter.firstName} ${cr.submitter.lastName}` : 'N/A'; - const reviewerName = - cr.reviewer?.firstName && cr.reviewer?.lastName ? `${cr.reviewer.firstName} ${cr.reviewer.lastName}` : 'N/A'; - - return ( - - - - CR #{cr.identifier.toLocaleString()} - - - - - Submitter: {submitterName} {' · '} Reviewer: {reviewerName} - - - {cr.wbsNum ? `${wbsPipe(cr.wbsNum)} - ` : ''} - {cr.wbsName ? `${cr.wbsName} · ` : ''} - {ChangeRequestTypeTextPipe(cr.type)} - - - ); -}; - -const GuestChangeRequestsPage: React.FC = () => { - const { data: allCrs, isLoading, isError, error } = useAllGuestChangeRequests(); - const [selectedTeamTypes, setSelectedTeamTypes] = useState([]); - const isMobilePortrait = useMediaQuery('(max-width:480px)'); - const { - isLoading: teamTypesIsLoading, - isError: teamTypesIsError, - data: teamTypes, - error: teamTypesError - } = useAllTeamTypes(); - - if (isLoading || !allCrs || teamTypesIsLoading || !teamTypes) return ; - if (isError) return ; - if (teamTypesIsError) return ; - - const filteredCrs = allCrs.filter( - (cr) => selectedTeamTypes.length === 0 || cr.teamTypeNames.some((name) => selectedTeamTypes.includes(name)) - ); - - return ( - - - {teamTypes.map((team) => ( - - setSelectedTeamTypes((prev) => - prev.includes(team.name) ? prev.filter((t: string) => t !== team.name) : [...(prev || []), team.name] - ) - } - clickable - color={selectedTeamTypes.includes(team.name) ? 'primary' : 'default'} - sx={{ flexShrink: 0 }} - /> - ))} - - - {filteredCrs.map((changeRequest) => ( - - ))} - - - ); -}; - -export default GuestChangeRequestsPage; diff --git a/src/frontend/src/pages/CreateChangeRequestPage/CreateChangeRequest.tsx b/src/frontend/src/pages/CreateChangeRequestPage/CreateChangeRequest.tsx index 621dde59e8..4ad21a83c7 100644 --- a/src/frontend/src/pages/CreateChangeRequestPage/CreateChangeRequest.tsx +++ b/src/frontend/src/pages/CreateChangeRequestPage/CreateChangeRequest.tsx @@ -115,7 +115,7 @@ const CreateChangeRequest: React.FC = () => { toast.error(e.message); } } finally { - if (requestHasASolution) history.push(`${routes.PROJECTS}/${wbsNum}/change-requests`); + if (requestHasASolution) history.push(routes.CHANGE_REQUESTS); } }; diff --git a/src/frontend/src/pages/CreditsPage/CreditsPage.tsx b/src/frontend/src/pages/CreditsPage/CreditsPage.tsx index 91ec8e95ba..5211316120 100644 --- a/src/frontend/src/pages/CreditsPage/CreditsPage.tsx +++ b/src/frontend/src/pages/CreditsPage/CreditsPage.tsx @@ -66,7 +66,6 @@ const CreditsPage: React.FC = () => { { name: 'Jared Ritchie', color: '#f0354e' }, { name: 'Alan Zhan', color: '#7AD0AC' }, { name: 'Sutton Spindler', color: '#53A3ff' }, - { name: 'Vanessa Fobid', color: '#a30062' }, { name: 'Emma Vonbuelow', color: '#c77ad0' }, { name: 'Aidan Roche', color: '#20B1AA' }, { name: 'Carrie Wang', color: '#f9cfc8' }, @@ -77,7 +76,6 @@ const CreditsPage: React.FC = () => { { name: 'Martin Hema', color: '#9125cc' }, { name: 'Shree Singhal', color: '#ff7ca4' }, { name: 'Isaac Levine', color: '#6a3941' }, - { name: 'Pooja Ramakrishnan', color: '#9125cc' }, { name: 'Andrew Tsai', color: '#3281a8' }, { name: 'Ahnaf Inkiad', color: '#ab38b5' }, { name: 'Aaryan Jain', color: '#e53774' }, @@ -124,7 +122,6 @@ const CreditsPage: React.FC = () => { { name: 'Raghav Mathur', color: '#009933' }, { name: 'Anika Sharma', color: '#ff0000' }, { name: 'William (Jack) Turner', color: '#ff5733' }, - { name: 'Natasha Joshi', color: '#00f7ffff' }, { name: 'Samson Ajayi', color: '6a0dad', @@ -257,7 +254,6 @@ const CreditsPage: React.FC = () => { { name: 'Amber Friar', color: '#F5A9B8' }, { name: 'Kaung Mo', color: '#9a1115' }, { name: 'Mae Balesterri', color: '#7fb2bc' }, - { name: 'Nigel Purvis', color: '#E2725B' }, { name: 'Joshua Goldberg', color: 'transparent', @@ -374,11 +370,7 @@ const CreditsPage: React.FC = () => { } }, { name: 'Josh Len', color: '#000000ff' }, - { name: 'Deepika Arulselvan', color: '#ad56fe' }, - { name: 'Grace Theobald', color: '#537c2c' }, - { name: 'Jasper Pinkus', color: '#276221' }, - { name: 'Hamilton LaPides', color: '#55a50a' }, - { name: 'Sara Johnson', color: '#ffff99' } + { name: 'Grace Theobald', color: '#537c2c' } ]; const snark = ['Add your name!', "Shouldn't you do it yourself?", 'Seriously', 'go', 'do', 'it']; diff --git a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx index 61ce83e89b..14396e1a0b 100644 --- a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx @@ -231,7 +231,6 @@ const FinancePieChart: React.FC = ({ /> = ({ startDate, endDate }) => { +const AdminFinanceDashboard: React.FC = ({ startDate, endDate, carNumber }) => { const user = useCurrentUser(); const [anchorEl, setAnchorEl] = useState(null); const [tabIndex, setTabIndex] = useState(0); const [showPendingAdvisorListModal, setShowPendingAdvisorListModal] = useState(false); const [showTotalAmountSpent, setShowTotalAmountSpent] = useState(false); - - const filter = useFinanceDashboardCarFilter(startDate, endDate); + const [startDateState, setStartDateState] = useState(startDate); + const [endDateState, setEndDateState] = useState(endDate); + const [carNumberState, setCarNumberState] = useState(carNumber); const { data: allTeamTypes, @@ -58,8 +59,16 @@ const AdminFinanceDashboard: React.FC = ({ startDate error: allPendingAdvisorListError } = useGetPendingAdvisorList(); - if (filter.error) { - return ; + const { data: allCars, isLoading: allCarsIsLoading, isError: allCarsIsError, error: allCarsError } = useGetAllCars(); + + useEffect(() => { + if (carNumberState === undefined && allCars && allCars.length > 0) { + setCarNumberState(allCars[allCars.length - 1].wbsNum.carNumber); + } + }, [allCars, carNumberState]); + + if (allCarsIsError) { + return ; } if (allTeamTypesIsError) { @@ -81,19 +90,16 @@ const AdminFinanceDashboard: React.FC = ({ startDate allReimbursementRequestsIsLoading || !allPendingAdvisorList || allPendingAdvisorListIsLoading || - filter.isLoading + !allCars || + allCarsIsLoading ) { return ; } - const ALL_CARS_ID = '__ALL_CARS__'; - const { selectedCar, allCars } = filter; - const sortedCars = [...allCars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber); - const carOptions = sortedCars.map((car) => ({ - label: car.wbsNum.carNumber === 0 ? car.name : `${car.name} (Car ${car.wbsNum.carNumber})`, - id: car.id + const carAutocompleteOptions = allCars.map((car) => ({ + label: car.name, + id: car.wbsNum.carNumber.toString() })); - const carAutocompleteOptions = [{ label: 'All Cars', id: ALL_CARS_ID }, ...carOptions]; const tabs = []; @@ -203,94 +209,56 @@ const AdminFinanceDashboard: React.FC = ({ startDate ml: 'auto' }} > - - { - if (newValue === null) { - // Cleared (X button) — re-mirror global - filter.clearLocalSelection(); - } else if (newValue.id === ALL_CARS_ID) { - // Explicit "All Cars" override - filter.setSelectedCar('all-cars'); - } else { - const car = allCars.find((c) => c.id === newValue.id); - if (car) filter.setSelectedCar(car); - } - }} - options={carAutocompleteOptions} - size="small" - placeholder="Select A Car" - value={ - selectedCar === 'all-cars' - ? { label: 'All Cars', id: ALL_CARS_ID } - : (carOptions.find((car) => car.id === selectedCar.id) ?? null) - } - sx={datePickerStyle} - /> - - - - - - (filter.endDate ? date > filter.endDate : false)} - slotProps={{ - textField: { - size: 'small', - sx: datePickerStyle - }, - field: { clearable: true } - }} - onChange={(newValue: Date | null) => filter.setStartDate(newValue ?? undefined)} - /> - - - - + setCarNumberState(newValue ? Number(newValue.id) : undefined)} + options={carAutocompleteOptions} + size="small" + placeholder="Select A Car" + value={ + carNumberState !== undefined ? carAutocompleteOptions.find((car) => car.id === carNumberState.toString()) : null + } + sx={datePickerStyle} + /> + (endDateState ? date > endDateState : false)} + slotProps={{ + textField: { + size: 'small', + sx: datePickerStyle + }, + field: { clearable: true } + }} + onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)} + /> - - - (filter.startDate ? date < filter.startDate : false)} - slotProps={{ - textField: { - size: 'small', - sx: datePickerStyle - }, - field: { clearable: true } - }} - onChange={(newValue: Date | null) => filter.setEndDate(newValue ?? undefined)} - /> - - - - + (startDateState ? date < startDateState : false)} + slotProps={{ + textField: { + size: 'small', + sx: datePickerStyle + }, + field: { clearable: true } + }} + onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)} + /> } variant="contained" id="project-actions-dropdown" onClick={handleClick} - sx={{ flexShrink: 0 }} > Actions @@ -300,14 +268,14 @@ const AdminFinanceDashboard: React.FC = ({ startDate handleDropdownClose(); setShowPendingAdvisorListModal(true); }} - disabled={!isFinance && !isAdmin(user.role)} + disabled={!isFinance && !isAdmin} > Pending Advisor List - setShowTotalAmountSpent(true)} disabled={!isFinance && !isAdmin(user.role)}> + setShowTotalAmountSpent(true)} disabled={!isFinance && !isAdmin}> @@ -351,24 +319,16 @@ const AdminFinanceDashboard: React.FC = ({ startDate /> )} {tabIndex === 0 ? ( - + ) : tabIndex === tabs.length - 1 ? ( - + ) : ( selectedTab && ( ) )} diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardAllView.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardAllView.tsx index 845f43a60d..03ffa2de61 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardAllView.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardAllView.tsx @@ -9,11 +9,11 @@ import AdminBalance from './AdminBalance'; interface FinanceDashboardAllViewProps { startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } -const FinanceDashboardAllView: React.FC = ({ startDate, endDate, overrideCarId }) => { - const payload = { startDate, endDate, overrideCarId }; +const FinanceDashboardAllView: React.FC = ({ startDate, endDate, carNumber }) => { + const payload = { startDate, endDate, carNumber }; // this hook returns the all data then budget data then cash data const { data: allRRData, diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardCategoriesView.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardCategoriesView.tsx index 102ba8dcf9..8b76dc0d87 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardCategoriesView.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardCategoriesView.tsx @@ -8,28 +8,24 @@ import AdminBalance from './AdminBalance'; interface FinanceDashboardCategoryViewProps { startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } -const FinanceDashboardCategoriesView: React.FC = ({ - startDate, - endDate, - overrideCarId -}) => { +const FinanceDashboardCategoriesView: React.FC = ({ startDate, endDate, carNumber }) => { // this hook returns the all data then budget data then cash data const { data: rrData, isLoading: rrDataIsLoading, isError: rrDataIsError, error: rrDataError - } = useGetAllReimbursementRequestData({ startDate, endDate, overrideCarId }); + } = useGetAllReimbursementRequestData({ startDate, endDate, carNumber }); const { data: spendingBarData, isLoading: spendingBarDataIsLoading, isError: spendingBarDataIsError, error: spendingBarDataError - } = useGetSpendingBarCategoryData({ startDate, endDate, overrideCarId }); + } = useGetSpendingBarCategoryData({ startDate, endDate, carNumber }); if (rrDataIsError) { return ; diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamTypeView.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamTypeView.tsx index 53fa376062..2c4003aa22 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamTypeView.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamTypeView.tsx @@ -9,27 +9,27 @@ interface FinanceDashboardTeamTypeViewProps { teamTypeId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } const FinanceDashboardTeamView: React.FC = ({ teamTypeId, startDate, endDate, - overrideCarId + carNumber }) => { const { data: rrData, isLoading: rrDataIsLoading, isError: rrDataIsError, error: rrDataError - } = useGetReimbursementRequestTeamTypeData({ teamTypeId, startDate, endDate, overrideCarId }); + } = useGetReimbursementRequestTeamTypeData({ teamTypeId, startDate, endDate, carNumber }); const { data: spendingBarData, isLoading: spendingBarDataIsLoading, isError: spendingBarDataIsError, error: spendingBarDataError - } = useGetSpendingBarTeamTypeData({ teamTypeId, startDate, endDate, overrideCarId }); + } = useGetSpendingBarTeamTypeData({ teamTypeId, startDate, endDate, carNumber }); if (rrDataIsError) { return ; diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamView.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamView.tsx index 4e8c302315..da41535866 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamView.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/FinanceDashboardTeamView.tsx @@ -9,28 +9,23 @@ interface FinanceDashboardTeamViewProps { teamId: string; startDate?: Date; endDate?: Date; - overrideCarId?: string | 'all-cars'; + carNumber?: number; } -const FinanceDashboardTeamView: React.FC = ({ - teamId, - startDate, - endDate, - overrideCarId -}) => { +const FinanceDashboardTeamView: React.FC = ({ teamId, startDate, endDate, carNumber }) => { const { data: rrData, isLoading: rrDataIsLoading, isError: rrDataIsError, error: rrDataError - } = useGetReimbursementRequestTeamData({ teamId, startDate, endDate, overrideCarId }); + } = useGetReimbursementRequestTeamData({ teamId, startDate, endDate, carNumber }); const { data: spendingBarData, isLoading: spendingBarDataIsLoading, isError: spendingBarDataIsError, error: spendingBarDataError - } = useGetSpendingBarTeamData({ teamId, startDate, endDate, overrideCarId }); + } = useGetSpendingBarTeamData({ teamId, startDate, endDate, carNumber }); if (rrDataIsError) { return ; diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/GeneralFinanceDashboard.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/GeneralFinanceDashboard.tsx index cb67562351..feb70ea017 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/GeneralFinanceDashboard.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/GeneralFinanceDashboard.tsx @@ -5,20 +5,23 @@ import PageLayout from '../../../components/PageLayout'; import { Box } from '@mui/system'; import FullPageTabs from '../../../components/FullPageTabs'; import { routes } from '../../../utils/routes'; +import { DatePicker } from '@mui/x-date-pickers'; import { useGetUsersTeams } from '../../../hooks/teams.hooks'; import FinanceDashboardTeamView from './FinanceDashboardTeamView'; -import FinanceDashboardCarFilter from '../../../components/FinanceDashboardCarFilter'; -import { useFinanceDashboardCarFilter } from '../../../hooks/finance-car-filter.hooks'; +import { useGetAllCars } from '../../../hooks/cars.hooks'; +import NERAutocomplete from '../../../components/NERAutocomplete'; interface GeneralFinanceDashboardProps { startDate?: Date; endDate?: Date; + carNumber?: number; } -const GeneralFinanceDashboard: React.FC = ({ startDate, endDate }) => { +const GeneralFinanceDashboard: React.FC = ({ startDate, endDate, carNumber }) => { const [tabIndex, setTabIndex] = useState(0); - - const filter = useFinanceDashboardCarFilter(startDate, endDate); + const [startDateState, setStartDateState] = useState(startDate); + const [endDateState, setEndDateState] = useState(endDate); + const [carNumberState, setCarNumberState] = useState(carNumber); const { data: allTeams, @@ -27,34 +30,159 @@ const GeneralFinanceDashboard: React.FC = ({ start error: allTeamsError } = useGetUsersTeams(); + const { data: allCars, isLoading: allCarsIsLoading, isError: allCarsIsError, error: allCarsError } = useGetAllCars(); + + if (allCarsIsError) { + return ; + } + if (allTeamsIsError) { return ; } - if (!allTeams || allTeamsIsLoading || filter.isLoading) { + if (!allTeams || allTeamsIsLoading || !allCars || allCarsIsLoading) { return ; } - if (filter.error) { - return ; - } + const carAutocompleteOptions = allCars.map((car) => { + return { + label: car.name, + id: car.id, + number: car.wbsNum.carNumber + }; + }); + + const datePickerStyle = { + width: 180, + height: 36, + color: 'white', + fontSize: '13px', + textTransform: 'none', + fontWeight: 400, + borderRadius: '4px', + boxShadow: 'none', + + '.MuiInputBase-root': { + height: '36px', + padding: '0 8px', + backgroundColor: '#ef4345', + color: 'white', + fontSize: '13px', + borderRadius: '4px', + '&:hover': { + backgroundColor: '#ef4345' + }, + '&.Mui-focused': { + backgroundColor: '#ef4345', + color: 'white' + } + }, + + '.MuiInputLabel-root': { + color: 'white', + fontSize: '14px', + transform: 'translate(15px, 7px) scale(1)', + '&.Mui-focused': { + color: 'white' + } + }, - const filterComponent = ( + '.MuiInputLabel-shrink': { + transform: 'translate(14px, -6px) scale(0.75)', + color: 'white' + }, + + '& .MuiInputBase-input': { + color: 'white', + paddingTop: '8px', + cursor: 'pointer', + '&:focus': { + color: 'white' + } + }, + + '& .MuiOutlinedInput-notchedOutline': { + border: '1px solid #fff', + '&:hover': { + borderColor: '#fff' + }, + '&.Mui-focused': { + borderColor: '#fff' + } + }, + + '& .MuiSvgIcon-root': { + color: 'white', + '&:hover': { + color: 'white' + }, + '&.Mui-focused': { + color: 'white' + } + } + }; + + const dates = ( - + setCarNumberState(newValue ? Number(newValue.id) : undefined)} + options={carAutocompleteOptions} + size="small" + placeholder="Select A Car" + value={ + carNumberState !== undefined ? carAutocompleteOptions.find((car) => car.id === carNumberState.toString()) : null + } + sx={datePickerStyle} + /> + (endDateState ? date > endDateState : false)} + slotProps={{ + textField: { + size: 'small', + sx: datePickerStyle + }, + field: { clearable: true } + }} + onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)} + /> + + + - + + + (startDateState ? date < startDateState : false)} + slotProps={{ + textField: { + size: 'small', + sx: datePickerStyle + }, + field: { clearable: true } + }} + onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)} + /> ); if (allTeams.length === 0) { return ( - + ); @@ -62,13 +190,13 @@ const GeneralFinanceDashboard: React.FC = ({ start if (allTeams.length === 1) { return ( - + ); @@ -86,7 +214,7 @@ const GeneralFinanceDashboard: React.FC = ({ start return ( = ({ start {selectedTab && ( )} diff --git a/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementFormView.tsx b/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementFormView.tsx index 069767f5f9..0abf5f997a 100644 --- a/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementFormView.tsx +++ b/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementFormView.tsx @@ -71,7 +71,7 @@ interface ReimbursementRequestFormViewProps { reimbursementProducts: ReimbursementProductFormArgs[]; receiptPrepend: (args: ReimbursementReceiptUploadArgs) => void; receiptRemove: (index: number) => void; - reimbursementProductPrepend: (args: ReimbursementProductFormArgs) => void; + reimbursementProductAppend: (args: ReimbursementProductFormArgs) => void; reimbursementProductRemove: (index: number) => void; onSubmit: (data: ReimbursementRequestFormInput) => void; handleSubmit: UseFormHandleSubmit; @@ -98,7 +98,7 @@ const ReimbursementRequestFormView: React.FC control, receiptPrepend, receiptRemove, - reimbursementProductPrepend, + reimbursementProductAppend, reimbursementProductRemove, onSubmit, handleSubmit, @@ -285,7 +285,7 @@ const ReimbursementRequestFormView: React.FC {/* Left Column */} - + {/* Vendor */} /> - {/* Account Code */} - - - Account Code* - - { - const mappedAccountCodes = allAccountCodes - .filter((accountCode) => accountCode.allowed) - .map(accountCodesToAutocomplete); - return ( - - ); - }} - /> - {errors.accountCodeId?.message} - + /> + )} + /> + + )} {/* Refund Sources */} @@ -620,69 +627,8 @@ const ReimbursementRequestFormView: React.FC {/* Right Column */} - - {/* Date of Expense */} - {(isHead(user.role) || (isEditing && isLeadershipApproved)) && ( - - - - Date of Expense{isLeadershipApproved ? '*' : ''} - - - - - - ( - setDatePickerOpen(false)} - onOpen={() => setDatePickerOpen(true)} - onChange={(newValue) => { - onChange(newValue ?? new Date()); - }} - slotProps={{ - textField: { - error: !!errors.dateOfExpense, - helperText: errors.dateOfExpense?.message, - variant: 'outlined', - fullWidth: true, - size: 'small', - InputProps: { - onClick: (e) => { - const target = e.target as HTMLElement; - if (target.closest('button')) { - setDatePickerOpen(true); - } - } - } - } - }} - /> - )} - /> - - )} - - {/* Description */} + + {/* Account Code */} textDecoration: 'underline', textUnderlineOffset: '3.5px', textDecorationThickness: '0.6px', + paddingBottom: '2px', fontSize: 'x-large', fontWeight: 'bold' }} > - Description + Account Code* ( - - )} + render={({ field: { onChange, value } }) => { + const mappedAccountCodes = allAccountCodes + .filter((accountCode) => accountCode.allowed) + .map(accountCodesToAutocomplete); + return ( + + ); + }} /> + {errors.accountCodeId?.message} {/* Upload Receipts */} @@ -888,6 +857,37 @@ const ReimbursementRequestFormView: React.FC + + {/* Description */} + + + Description + + ( + + )} + /> +
@@ -900,7 +900,7 @@ const ReimbursementRequestFormView: React.FC void; - prependProduct: (args: ReimbursementProductFormArgs) => void; + appendProduct: (args: ReimbursementProductFormArgs) => void; projectAutocompleteOptions: { label: string; id: string; @@ -145,7 +145,7 @@ const MaterialAutocomplete: React.FC<{ const ReimbursementProductTable: React.FC = ({ reimbursementProducts, removeProduct, - prependProduct, + appendProduct, projectAutocompleteOptions, control, errors, @@ -403,7 +403,7 @@ const ReimbursementProductTable: React.FC = ({ options={projectAutocompleteOptions} onChange={(_e, value) => { if (value) { - prependProduct({ + appendProduct({ reason: validateWBS(value.id), name: '', cost: 0, @@ -428,7 +428,7 @@ const ReimbursementProductTable: React.FC = ({ getOptionLabel={(option) => formatReasonName(option.name)} onChange={(_e, value) => { if (value) { - prependProduct({ + appendProduct({ reason: value, name: '', cost: 0, @@ -834,7 +834,7 @@ const ReimbursementProductTable: React.FC = ({ onClick={(e) => { const existingProducts = uniqueWbsElementsWithProducts.get(key); if (existingProducts && existingProducts.length > 0) { - prependProduct({ + appendProduct({ reason: existingProducts[0].reason, name: '', cost: 0, diff --git a/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementRequestForm.tsx b/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementRequestForm.tsx index 95c8c82e0a..f6e0dfa229 100644 --- a/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementRequestForm.tsx +++ b/src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementRequestForm.tsx @@ -180,7 +180,7 @@ const ReimbursementRequestForm: React.FC = ({ }); const { fields: reimbursementProducts, - prepend: reimbursementProductPrepend, + append: reimbursementProductAppend, remove: reimbursementProductRemove } = useFieldArray({ control, @@ -356,7 +356,7 @@ const ReimbursementRequestForm: React.FC = ({ reimbursementProducts={reimbursementProducts} receiptPrepend={receiptPrepend} receiptRemove={receiptRemove} - reimbursementProductPrepend={reimbursementProductPrepend} + reimbursementProductAppend={reimbursementProductAppend} reimbursementProductRemove={reimbursementProductRemove} onSubmit={onSubmitWrapper} handleSubmit={handleSubmit} diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartCollectionSection.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartCollectionSection.tsx index f5ed6981f8..5223fdc373 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartCollectionSection.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartCollectionSection.tsx @@ -1,8 +1,8 @@ import { Edit } from '@mui/icons-material'; import { Box, Chip, IconButton, Typography, useTheme } from '@mui/material'; import GanttChartSection from './GanttChartSection'; -import { GanttCollection } from '../../../utils/gantt.utils'; -import { useState } from 'react'; +import { GanttCollection, GanttTask } from '../../../utils/gantt.utils'; +import { useCallback, useState } from 'react'; import { GanttEditability } from './GanttChart'; interface GanttChartCollectionSectionProps { @@ -57,11 +57,16 @@ const GanttChartCollectionSection = ({ setIsEditMode(true); }; - const ignore = () => {}; + // Sorting the work packages of each project based on their start date + collection.tasks.forEach((task) => { + task.children.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()); + }); - const ignoreBool = () => false; + const ignore = useCallback(() => {}, []); - return collection.tasks.length > 0 ? ( + const ignoreBool = useCallback(() => false, []); + + return ( @@ -97,7 +102,7 @@ const GanttChartCollectionSection = ({ /> - ) : null; + ); }; export default GanttChartCollectionSection; diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx index 7b281698a1..d7af1e2511 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx @@ -5,6 +5,10 @@ import GanttTaskBarEdit from './GanttTaskBarEdit'; import GanttTaskBarView from './GanttTaskBarView'; +import { ArcherContainer } from 'react-archer'; +import { useCallback } from 'react'; +import { useRef } from 'react'; +import { ArcherContainerHandle } from 'react-archer/lib/ArcherContainer/ArcherContainer.types'; import { GanttChange, GanttTask, @@ -42,18 +46,27 @@ const GanttTaskBar = ({ highlightTaskComparator, onToggle }: GanttTaskBarProps) => { - const getStartCol = (start: Date) => { - const startCol = days.findIndex((day) => toDateString(day) === toDateString(getMonday(start))) + 1; - return startCol; - }; + const archerRef = useRef(null); - const getEndCol = (end: Date) => { - const endCol = - days.findIndex((day) => toDateString(day) === toDateString(getMonday(end))) === -1 - ? days.length + 1 - : days.findIndex((day) => toDateString(day) === toDateString(getMonday(end))) + 2; - return endCol; - }; + const getStartCol = useCallback( + (start: Date) => { + const startCol = days.findIndex((day) => day.toDateString() === getMonday(start).toDateString()) + 1; + return startCol; + }, + [days] + ); + + // if the end date doesn't exist within the timeframe, have it span to the end + const getEndCol = useCallback( + (end: Date) => { + const endCol = + days.findIndex((day) => day.toDateString() === getMonday(end).toDateString()) === -1 + ? days.length + 1 + : days.findIndex((day) => day.toDateString() === getMonday(end).toDateString()) + 2; + return endCol; + }, + [days] + ); return (
diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplay.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplay.tsx index c9ae3b426c..2e36957cc0 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplay.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplay.tsx @@ -19,7 +19,7 @@ import { webKitBoxContainerStyles, webKitBoxStyles } from './GanttTaskBarDisplayStyles'; -import { CSSProperties } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { ArcherElement } from 'react-archer'; interface GanttTaskBarDisplayProps { @@ -78,74 +78,86 @@ const GanttTaskBarDisplay = ({ width: hasOverlays ? 'fit-content' : '100%' }; - const ganttTaskBarChildOverlayStyles = (child: GanttTask): CSSProperties => { - return { - position: 'absolute', - left: `calc(${getStartCol(child.start) - 1} * (${GANTT_CHART_CELL_SIZE} + ${GANTT_CHART_GAP_SIZE}))`, - width: `calc(${getEndCol(child.end) - getStartCol(child.start)} * (${GANTT_CHART_CELL_SIZE} + ${GANTT_CHART_GAP_SIZE}) - ${GANTT_CHART_GAP_SIZE})`, - height: '2rem', - border: `1px solid ${theme.palette.divider}`, - borderRadius: '0.25rem', - backgroundColor: child.styles ? child.styles.backgroundColor : grey[700], - cursor: 'pointer', - gridRow: 1, - zIndex: 2 - }; - }; + const ganttTaskBarChildOverlayStyles = useCallback( + (child: GanttTask): CSSProperties => { + return { + position: 'absolute', + left: `calc(${getStartCol(child.start) - 1} * (${GANTT_CHART_CELL_SIZE} + ${GANTT_CHART_GAP_SIZE}))`, + width: `calc(${getEndCol(child.end) - getStartCol(child.start)} * (${GANTT_CHART_CELL_SIZE} + ${GANTT_CHART_GAP_SIZE}) - ${GANTT_CHART_GAP_SIZE})`, + height: '2rem', + border: `1px solid ${theme.palette.divider}`, + borderRadius: '0.25rem', + backgroundColor: child.styles ? child.styles.backgroundColor : grey[700], + cursor: 'pointer', + gridRow: 1, + zIndex: 2 + }; + }, + [theme.palette.divider] + ); - const ganttTaskBarEventOverlayStyles = (event: GanttEvent): CSSProperties => { - return { - gridColumnStart: getStartCol(event.date), - gridColumnEnd: getEndCol(addWeeksToDate(event.date, 1)), - height: '2rem', - border: `1px solid ${theme.palette.divider}`, - borderRadius: '0.25rem', - backgroundColor: event.color, - cursor: 'pointer', - gridRow: 1, - zIndex: 5 - }; - }; + const ganttTaskBarEventOverlayStyles = useCallback( + (event: GanttEvent): CSSProperties => { + return { + gridColumnStart: getStartCol(event.date), + gridColumnEnd: getEndCol(addWeeksToDate(event.date, 1)), + height: '2rem', + border: `1px solid ${theme.palette.divider}`, + borderRadius: '0.25rem', + backgroundColor: event.color, + cursor: 'pointer', + gridRow: 1, + zIndex: 5 + }; + }, + [theme.palette.divider] + ); - const highlightedChangeBoxStyles = (highlightedChange: RequestEventChange): CSSProperties => { - return { - paddingTop: '2px', - paddingLeft: '5px', - gridColumnStart: getStartCol(highlightedChange.newStart), - gridColumnEnd: getEndCol(highlightedChange.newEnd), - height: '2rem', - border: `1px solid ${theme.palette.text.primary}`, - borderRadius: '0.25rem', - backgroundColor: '#ef4345', - cursor: 'pointer', - gridRow: 1, - zIndex: 6 - }; - }; + const highlightedChangeBoxStyles = useCallback( + (highlightedChange: RequestEventChange): CSSProperties => { + return { + paddingTop: '2px', + paddingLeft: '5px', + gridColumnStart: getStartCol(highlightedChange.newStart), + gridColumnEnd: getEndCol(highlightedChange.newEnd), + height: '2rem', + border: `1px solid ${theme.palette.text.primary}`, + borderRadius: '0.25rem', + backgroundColor: '#ef4345', + cursor: 'pointer', + gridRow: 1, + zIndex: 6 + }; + }, + [theme.palette.text.primary] + ); - const retroOverlayBoxStyles = (retro: { comparativeStart?: Date; comparativeEnd?: Date }): CSSProperties => { - if (!retro.comparativeStart || !retro.comparativeEnd) { - return {}; - } + const retroOverlayBoxStyles = useCallback( + (retro: { comparativeStart?: Date; comparativeEnd?: Date }): CSSProperties => { + if (!retro.comparativeStart || !retro.comparativeEnd) { + return {}; + } - return { - paddingTop: '2px', - paddingLeft: '5px', - gridColumnStart: getStartCol(retro.comparativeStart), - gridColumnEnd: getEndCol(retro.comparativeEnd), - height: '2rem', - border: `1px solid ${theme.palette.text.primary}`, - borderRadius: '0.25rem', - backgroundImage: ` + return { + paddingTop: '2px', + paddingLeft: '5px', + gridColumnStart: getStartCol(retro.comparativeStart), + gridColumnEnd: getEndCol(retro.comparativeEnd), + height: '2rem', + border: `1px solid ${theme.palette.text.primary}`, + borderRadius: '0.25rem', + backgroundImage: ` repeating-linear-gradient(-45deg, #000 0, #000 1px, transparent 1px, transparent 10px) `, - backgroundColor: grey[100], - opacity: 0.3, - cursor: 'pointer', - gridRow: 1, - zIndex: 1 - }; - }; + backgroundColor: grey[100], + opacity: 0.3, + cursor: 'pointer', + gridRow: 1, + zIndex: 1 + }; + }, + [theme.palette.text.primary, grey[100]] + ); return (
diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplayStyles.ts b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplayStyles.ts index cffdfc6af4..2d6e9f3de2 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplayStyles.ts +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarDisplayStyles.ts @@ -1,7 +1,7 @@ -import { CSSProperties } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { GanttTask, GANTT_CHART_CELL_SIZE, GANTT_CHART_GAP_SIZE } from '../../../../../utils/gantt.utils'; -export const ganttTaskBarBackgroundStyles = (numDays: number): CSSProperties => { +export const ganttTaskBarBackgroundStyles = useCallback((numDays: number): CSSProperties => { return { width: '100%', display: 'grid', @@ -12,17 +12,17 @@ export const ganttTaskBarBackgroundStyles = (numDays: number): CSSProperties => // top: 0, // left: 0 }; -}; +}, []); -export const ganttTaskBarContainerStyles = (): CSSProperties => { +export const ganttTaskBarContainerStyles = useCallback((): CSSProperties => { return { position: 'relative', width: '100%', marginTop: 10 }; -}; +}, []); -export const webKitBoxContainerStyles = (): CSSProperties => { +export const webKitBoxContainerStyles = useCallback((): CSSProperties => { return { height: '100%', width: '100%', @@ -31,9 +31,9 @@ export const webKitBoxContainerStyles = (): CSSProperties => { alignItems: 'center', overflow: 'visible' }; -}; +}, []); -export const webKitBoxStyles = (): CSSProperties => { +export const webKitBoxStyles = useCallback((): CSSProperties => { return { padding: '0.25rem', overflow: 'hidden', @@ -42,7 +42,7 @@ export const webKitBoxStyles = (): CSSProperties => { WebkitLineClamp: 1, userSelect: 'none' }; -}; +}, []); export const taskNameContainerStyles = (task: GanttTask): CSSProperties => { return { diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartSection.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartSection.tsx index 117c287c06..02d8b45e1d 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartSection.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartSection.tsx @@ -11,7 +11,7 @@ import { OnMouseOverOptions, RequestEventChange } from '../../../utils/gantt.utils'; -import { Box } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { MutableRefObject, useCallback, useRef, useState } from 'react'; import GanttTaskBar from './GanttChartComponents/GanttTaskBar/GanttTaskBar'; import GanttToolTip from './GanttChartComponents/GanttToolTip'; @@ -94,8 +94,8 @@ const GanttChartSection = ({ [createChange] ); - return ( - + return tasks.length > 0 ? ( + {tasks.map((task) => { @@ -121,6 +121,8 @@ const GanttChartSection = ({ + ) : ( + No Projects to Display ); }; diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/AddGanttTaskModal.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/AddGanttTaskModal.tsx index b4c2dc64fb..5bb7e9ad7d 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/AddGanttTaskModal.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/AddGanttTaskModal.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { yupResolver } from '@hookform/resolvers/yup'; import { FormControl, FormHelperText, FormLabel, MenuItem, TextField, Autocomplete, Grid } from '@mui/material'; import { Controller, useForm } from 'react-hook-form'; -import { countWords, isUnderWordCount, TaskPriority, TaskStatus } from 'shared'; +import { countWords, isUnderWordCount, TaskPriority, TaskStatus, User } from 'shared'; import * as yup from 'yup'; import NERFormModal from '../../../components/NERFormModal'; import { useAllMembers } from '../../../hooks/users.hooks'; @@ -40,7 +40,21 @@ interface AddGanttTaskModalProps { const AddGanttTaskModal: React.FC = ({ showModal, handleClose, addTask }) => { const { isLoading: usersIsLoading, isError: usersIsError, data: users, error: usersError } = useAllMembers(); - const unUpperCase = (str: string) => str.charAt(0) + str.slice(1).toLowerCase(); + if (!users || usersIsLoading) return ; + if (usersIsError) return ; + + return ; +}; + +interface AddGanttTaskModalContentProps { + showModal: boolean; + handleClose: () => void; + addTask: (task: CreateTaskFormData) => void; + users: User[]; +} + +const AddGanttTaskModalContent: React.FC = ({ showModal, handleClose, addTask, users }) => { + const unUpperCase = useCallback((str: string) => str.charAt(0) + str.slice(1).toLowerCase(), []); const { handleSubmit, @@ -60,9 +74,6 @@ const AddGanttTaskModal: React.FC = ({ showModal, handle } }); - if (!users || usersIsLoading) return ; - if (usersIsError) return ; - const options: { label: string; id: string }[] = users.map(taskUserToAutocompleteOption); const onSubmit = async (data: CreateTaskFormData) => { @@ -70,13 +81,10 @@ const AddGanttTaskModal: React.FC = ({ showModal, handle handleClose(); }; - const handleModalClose = () => { + const handleModalClose = useCallback(() => { reset(); handleClose(); - }; - - if (usersIsError) return ; - if (usersIsLoading) return ; + }, [reset, handleClose]); return ( ) => void; defaultChecked: boolean }[]; -}) => ( - - {handlers.map((handler) => ( - - } - label={handler.filterLabel} - /> - ))} - -); +import { Box, Checkbox, Chip, IconButton, Typography, useTheme } from '@mui/material'; +import RestartAltIcon from '@mui/icons-material/RestartAlt'; +import { ChangeEvent } from 'react'; -const FilterAccordion = ({ - label, - children, - expanded, - onChange +const FilterChipButton = ({ + buttonText, + onChange, + defaultChecked, + checked }: { - label: string; - children: React.ReactNode; - expanded: boolean; - onChange: () => void; + buttonText: string; + onChange: (event: ChangeEvent) => void; + defaultChecked: boolean; + checked: boolean; }) => { const theme = useTheme(); return ( - - } - sx={{ - minHeight: '36px', - px: 0, - flexDirection: 'row-reverse', - gap: 1, - '& .MuiAccordionSummary-expandIconWrapper': { marginRight: 0, marginLeft: 0 }, - '& .MuiAccordionSummary-content': { margin: '6px 0' } - }} - > - - {label} - - - {children} - + icon={} + checkedIcon={ + + } + defaultChecked={defaultChecked} + checked={checked} + /> + ); +}; + +const FilterRow = ({ + label, + buttons +}: { + label: string; + buttons: { filterLabel: string; handler: (event: ChangeEvent) => void; defaultChecked: boolean }[]; +}) => { + const checkedMap: { [filterLabel: string]: boolean } = {}; + + buttons.forEach((button) => { + checkedMap[button.filterLabel] = button.defaultChecked; + }); + return ( + + + {label} + + + {buttons.map((button) => ( + + ))} + + ); }; @@ -93,88 +77,83 @@ interface GanttChartFiltersProps { defaultChecked: boolean; }[]; teamHandlers: { filterLabel: string; handler: (event: ChangeEvent) => void; defaultChecked: boolean }[]; + overdueHandler: { + filterLabel: string; + handler: (event: ChangeEvent) => void; + defaultChecked?: boolean; + }[]; + hideTasksHandler: { + filterLabel: string; + handler: (event: ChangeEvent) => void; + defaultChecked?: boolean; + }[]; resetHandler: () => void; - onClose: () => void; } const GanttChartFilters = ({ carHandlers, teamTypeHandlers, teamHandlers, - resetHandler, - onClose + overdueHandler, + hideTasksHandler, + resetHandler }: GanttChartFiltersProps) => { - const theme = useTheme(); - - const [expanded, setExpanded] = useState({ - car: true, - division: true, - team: true - }); - - const [resetKey, setResetKey] = useState(0); - - const handleReset = () => { - setResetKey((prev) => prev + 1); - resetHandler(); - }; - - const toggle = (section: keyof typeof expanded) => { - setExpanded((prev) => ({ ...prev, [section]: !prev[section] })); - }; - - return ( - - - - Filters - - - + const FilterButtons = () => { + return ( + + + - - - - + Reset - - setExpanded({ car: true, division: true, team: true })} - > - Expand All + + + Overdue - - setExpanded({ car: false, division: false, team: false })} - > - Collapse All + + + Hide Tasks + ); + }; - toggle('car')}> - - - - toggle('division')}> - - - - toggle('team')}> - - + return ( + + + + + ); }; diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/GanttChartFiltersButton.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/GanttChartFiltersButton.tsx index ae6e0f7a11..b732ae18fb 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/GanttChartFiltersButton.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/GanttChartFiltersButton.tsx @@ -1,8 +1,7 @@ -import { ChangeEvent, useState } from 'react'; +import { ChangeEvent, useState, useCallback } from 'react'; import { IconButton, Popover } from '@mui/material'; import GanttChartFilters from './GanttChartFilters'; import { Tune } from '@mui/icons-material'; -import { useTheme } from '@mui/material'; interface GanttChartFiltersButtonProps { carHandlers: { filterLabel: string; handler: (event: ChangeEvent) => void; defaultChecked: boolean }[]; @@ -12,6 +11,16 @@ interface GanttChartFiltersButtonProps { defaultChecked: boolean; }[]; teamHandlers: { filterLabel: string; handler: (event: ChangeEvent) => void; defaultChecked: boolean }[]; + overdueHandler: { + filterLabel: string; + handler: (event: ChangeEvent) => void; + defaultChecked?: boolean; + }[]; + hideTasksHandler: { + filterLabel: string; + handler: (event: ChangeEvent) => void; + defaultChecked?: boolean; + }[]; resetHandler: () => void; } @@ -19,17 +28,18 @@ const GanttChartFiltersButton = ({ carHandlers, teamTypeHandlers, teamHandlers, + overdueHandler, + hideTasksHandler, resetHandler }: GanttChartFiltersButtonProps) => { - const theme = useTheme(); const [anchorFilterEl, setAnchorFilterEl] = useState(null); const handleFilterClick = (event: React.MouseEvent) => { setAnchorFilterEl(event.currentTarget); }; - const handleFilterClose = () => { + const handleFilterClose = useCallback(() => { setAnchorFilterEl(null); - }; + }, []); const open = Boolean(anchorFilterEl); return ( @@ -50,21 +60,14 @@ const GanttChartFiltersButton = ({ horizontal: 'right' }} sx={{ maxWidth: '100rem' }} - slotProps={{ - paper: { - sx: { - backgroundColor: theme.palette.background.paper, - borderRadius: 2 - } - } - }} > diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttProjectCreateModal.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttProjectCreateModal.tsx index 4c0319f1db..8dfee3f66d 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttProjectCreateModal.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttProjectCreateModal.tsx @@ -11,6 +11,7 @@ import { WorkPackageApiInputs } from '../../../../apis/work-packages.api'; import { useCreateSingleWorkPackage } from '../../../../hooks/work-packages.hooks'; import { GanttRequestChangeModalProps } from './GanttRequestChangeModal'; import { useCreateTask } from '../../../../hooks/tasks.hooks'; +import { useState } from 'react'; interface GanttProjectCreateModalProps extends GanttRequestChangeModalProps {} @@ -28,7 +29,7 @@ export const GanttProjectCreateModal = ({ change, handleClose, open }: GanttProj const changeInTimeline = `${dayjs(startDate).format('MMMM D, YYYY')} - ${dayjs(latestEndDate).format('MMMM D, YYYY')}`; const handleSubmit = async () => { - const [selectedTeam] = project.teams; + const [selectedTeam] = useState(project.teams[0]); const teamIds: string[] = selectedTeam ? [selectedTeam.teamId] : []; diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx index 995e8d1f37..1122bd9ee2 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx @@ -10,7 +10,7 @@ import { WbsElementPreview, WorkPackage } from 'shared'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import dayjs from 'dayjs'; import { CreateStandardChangeRequestPayload, useCreateStandardChangeRequest } from '../../../../hooks/change-requests.hooks'; import LoadingIndicator from '../../../../components/LoadingIndicator'; @@ -25,9 +25,7 @@ import { useSingleProject } from '../../../../hooks/projects.hooks'; interface GanttTimeLineChangeModalProps extends GanttRequestChangeModalProps {} export const GanttTimeLineChangeModal = ({ change, handleClose, open }: GanttTimeLineChangeModalProps) => { - const toast = useToast(); - const [reasonForChange, setReasonForChange] = useState(ChangeRequestReason.Estimation); - const [explanationForChange, setExplanationForChange] = useState(''); + const { data: originalProject, isLoading: originalProjectIsLoading, @@ -48,21 +46,46 @@ export const GanttTimeLineChangeModal = ({ change, handleClose, open }: GanttTim isLoadingTaskEdit ) return ; - if (originalProjectIsError) return ; + if (originalProjectIsError) return ; + return ; - const handleReasonChange = (event: SelectChangeEvent) => { +} + +interface GanttTimeLineChangeModalDataProps extends GanttRequestChangeModalProps { + originalProject: ProjectGantt; + createStandardChangeRequest: (payload: CreateStandardChangeRequestPayload) => Promise; + createSingleWorkPackage: (payload: any) => Promise; + createTask: (payload: CreateTaskPayload) => Promise; + editTask: (payload: TaskPayload) => Promise; +} + +export const GanttTimeLineChangeModalDataProps = ({originalProject, change, handleClose, open, createStandardChangeRequest, createSingleWorkPackage, createTask, editTask}: GanttTimeLineChangeModalDataProps) => { + const [reasonForChange, setReasonForChange] = useState(ChangeRequestReason.Estimation); + const [explanationForChange, setExplanationForChange] = useState(''); + const toast = useToast(); + const handleReasonChange = useCallback((event: SelectChangeEvent) => { setReasonForChange(event.target.value as ChangeRequestReason); - }; + }, []); - const handleExplanationChange = (event: React.ChangeEvent) => { + const handleExplanationChange = useCallback((event: React.ChangeEvent) => { setExplanationForChange(event.target.value); - }; + }, []); - const changeInTimeline = (startDate: Date, endDate: Date) => { + const changeInTimeline = useCallback((startDate: Date, endDate: Date) => { return `${dayjs(startDate).format('MMMM D, YYYY')} - ${dayjs(endDate).format('MMMM D, YYYY')}`; - }; + }, []); - const createWhatMessage = (editedWorkPackages: WorkPackage[]): string => { + const createWhatMessage = useCallback((editedWorkPackages: WorkPackage[]): string => { return ( 'Adjusted Timelines for WorkPackages: \n' + editedWorkPackages @@ -75,15 +98,15 @@ export const GanttTimeLineChangeModal = ({ change, handleClose, open }: GanttTim ) .join('\n') ); - }; + }, []); - const transformLinkToLinkCreateArgs = (link: Link): LinkCreateArgs => { + const transformLinkToLinkCreateArgs = useCallback((link: Link): LinkCreateArgs => { return { linkId: link.linkId, linkTypeName: link.linkType.name, url: link.url }; - }; + }, []); const project = change.element as ProjectGantt; diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChartPage.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChartPage.tsx index 2457c0736c..8cce3bf898 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChartPage.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChartPage.tsx @@ -3,7 +3,7 @@ * See the LICENSE file in the repository root folder for details. */ -import React, { ChangeEvent, FC, useEffect, useState } from 'react'; +import React, { ChangeEvent, FC, useEffect, useState, useCallback, useMemo } from 'react'; import LoadingIndicator from '../../../components/LoadingIndicator'; import { useAllProjectsGantt } from '../../../hooks/projects.hooks'; import ErrorPage from '../../ErrorPage'; @@ -30,6 +30,7 @@ import GanttChartColorLegend from './GanttChartColorLegend'; import GanttChartFiltersButton from './GanttChartFiltersButton'; import GanttChart from '../GanttChart/GanttChart'; import { + Car, ProjectGantt, Task, TaskPriority, @@ -43,8 +44,8 @@ import { WorkPackageStage } from 'shared'; import { useAllTeams } from '../../../hooks/teams.hooks'; +import { useGetAllCars } from '../../../hooks/cars.hooks'; import { useAllTeamTypes } from '../../../hooks/team-types.hooks'; -import { useGlobalCarFilter } from '../../../app/AppGlobalCarFilterContext'; import AddGanttProjectModal from './AddGanttProjectModal'; import AddGanttWorkPackageModal from './AddGanttWorkPackageModal'; import AddGanttSelectionModal from './AddGanttSelectionModal'; @@ -55,15 +56,13 @@ import { v4 as uuidv4 } from 'uuid'; import { projectWbsPipe } from '../../../utils/pipes'; import { projectGanttTransformer } from '../../../apis/transformers/projects.transformers'; import { useCurrentUser } from '../../../hooks/users.hooks'; +import { all } from 'axios'; const getElementId = (element: WbsElementPreview | Task) => { return (element as WbsElementPreview).id ?? (element as Task).taskId; }; const ProjectGanttChartPage: FC = () => { - const history = useHistory(); - const toast = useToast(); - const { isLoading: projectsIsLoading, isError: projectsIsError, @@ -71,6 +70,7 @@ const ProjectGanttChartPage: FC = () => { error: projectsError } = useAllProjectsGantt(); + /******************** Filters ***************************/ const { isLoading: teamTypesIsLoading, isError: teamTypesIsError, @@ -78,32 +78,46 @@ const ProjectGanttChartPage: FC = () => { error: teamTypesError } = useAllTeamTypes(); - const { selectedCar, allCars, isLoading: carFilterLoading } = useGlobalCarFilter(); + const { isLoading: carsIsLoading, isError: carsIsError, data: cars, error: carsError } = useGetAllCars(); const { isLoading: teamsIsLoading, isError: teamsIsError, data: teams, error: teamsError } = useAllTeams(); + + if ( + projectsIsLoading || + teamTypesIsLoading || + teamsIsLoading || + !teams || + !projects || + !teamTypes || + carsIsLoading || + !cars + ) + return ; + + if (projectsIsError) return ; + if (teamTypesIsError) return ; + if (teamsIsError) return ; + if (carsIsError) return ; + + return ; +}; + +interface ProjectGanttChartPageDataProps { + projects: ProjectGantt[]; + teams: TeamPreview[]; + teamTypes: TeamType[]; + cars: Car[]; +} + +const ProjectGanttChartPageData: FC = ({ projects, teams, teamTypes, cars }) => { + const history = useHistory(); + const toast = useToast(); + + const { filters, setFilters } = useGanttFilters('project-gantt'); const [searchText, setSearchText] = useState(''); const [addedProjects, setAddedProjects] = useState([]); - const [showAddProjectModal, setShowAddProjectModal] = useState(false); - const [showAddWorkPackageModal, setShowAddWorkPackageModal] = useState(false); - const [showAddTaskModal, setShowAddTaskModal] = useState(false); - const [showSelectionModal, setShowSelectionModal] = useState(false); - const [ganttChanges, setGanttChanges] = useState[]>([]); - const [requestEventChanges, setRequestEventChanges] = useState[]>([]); - const [selectedProject, setSelectedProject] = useState(undefined); - const [selectedTeam, setSelectedTeam] = useState(undefined); - const [collections, setCollections] = useState[]>([]); const [allProjects, setAllProjects] = useState([]); const [editedProjects, setEditedProjects] = useState([]); - const user = useCurrentUser(); - - /******************** Filters ***************************/ - const { filters, setFilters } = useGanttFilters('project-gantt'); - - // Local car filter state — resets to global selection whenever global car filter changes - const [showCars, setShowCars] = useState([]); - useEffect(() => { - if (carFilterLoading) return; - setShowCars(selectedCar === 'all-cars' ? allCars.map((car) => car.wbsNum.carNumber) : [selectedCar.wbsNum.carNumber]); - }, [carFilterLoading, selectedCar, allCars]); + const [collections, setCollections] = useState[]>([]); useEffect(() => { const requestRefresh = ( @@ -117,7 +131,6 @@ const ProjectGanttChartPage: FC = () => { let allProjects: ProjectGantt[] = JSON.parse(JSON.stringify(projects.concat(addedProjects))).map( projectGanttTransformer ); - allProjects = allProjects.map((project) => { const editedProject = editedProjects.find((proj) => proj.id === project.id); return editedProject ? editedProject : project; @@ -136,34 +149,34 @@ const ProjectGanttChartPage: FC = () => { }; if (projects && teams) { - requestRefresh(projects, teams, editedProjects, addedProjects, { ...filters, showCars }, searchText); + requestRefresh(projects, teams, editedProjects, addedProjects, filters, searchText); } - }, [ - teams, - projects, - addedProjects, - setAllProjects, - setCollections, - editedProjects, - filters, - showCars, - searchText, - history - ]); + }, [teams, projects, addedProjects, setAllProjects, setCollections, editedProjects, filters, searchText, history]); + + const [showWorkPackagesMap, setShowWorkPackagesMap] = useState>(new Map()); + + const [showAddProjectModal, setShowAddProjectModal] = useState(false); + const [showAddWorkPackageModal, setShowAddWorkPackageModal] = useState(false); + const [showAddTaskModal, setShowAddTaskModal] = useState(false); + const [showSelectionModal, setShowSelectionModal] = useState(false); + const [ganttChanges, setGanttChanges] = useState[]>([]); + const [requestEventChanges, setRequestEventChanges] = useState[]>([]); + const [selectedProject, setSelectedProject] = useState(undefined); + const [selectedTeam, setSelectedTeam] = useState(undefined); + + const user = useCurrentUser(); const handleSetGanttFilters = (newFilters: GanttFilters) => { setFilters(newFilters); }; - if (projectsIsLoading || teamTypesIsLoading || teamsIsLoading || carFilterLoading || !teams || !projects || !teamTypes) - return ; - if (projectsIsError) return ; - if (teamTypesIsError) return ; - if (teamsIsError) return ; - const carFilterHandler = (car: number) => { return (event: ChangeEvent) => { - setShowCars((prev) => (event.target.checked ? Array.from(new Set([...prev, car])) : prev.filter((c) => c !== car))); + handleSetGanttFilters( + event.target.checked + ? { ...filters, showCars: Array.from(new Set([...filters.showCars, car])) } + : { ...filters, showCars: filters.showCars.filter((c) => c !== car) } + ); }; }; @@ -214,49 +227,64 @@ const ProjectGanttChartPage: FC = () => { }; }); + const overdueHandler = [ + { + filterLabel: 'Overdue', + handler: (event: ChangeEvent) => + handleSetGanttFilters({ ...filters, showOnlyOverdue: event.target.checked }), + defaultChecked: filters.showOnlyOverdue + } + ]; + + const hideTasksHandler = [ + { + filterLabel: 'Hide Tasks', + handler: (event: ChangeEvent) => + handleSetGanttFilters({ ...filters, hideTasks: event.target.checked }), + defaultChecked: filters.hideTasks + } + ]; + const carHandlers: { filterLabel: string; handler: (event: ChangeEvent) => void; defaultChecked: boolean; - }[] = [...allCars] - .sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber) - .map((car) => { - const carNum = car.wbsNum.carNumber; - return { - filterLabel: car.name, - handler: carFilterHandler(carNum), - defaultChecked: showCars.includes(carNum) - }; - }); + }[] = cars.map((car) => { + const carNum = car.wbsNum.carNumber; + return { + filterLabel: carNum === 0 ? 'None' : `Car ${carNum}`, + handler: carFilterHandler(carNum), + defaultChecked: filters.showCars.includes(carNum) + }; + }); const resetHandler = () => { history.push(routes.GANTT); localStorage.removeItem('ganttURL'); - setShowCars(selectedCar === 'all-cars' ? allCars.map((car) => car.wbsNum.carNumber) : [selectedCar.wbsNum.carNumber]); }; /* **************************************************** */ /* ****************** Editability ********************* */ - const handleCancel = (_collection?: GanttCollection) => { + const handleCancel = useCallback((_collection?: GanttCollection) => { //TODO Filter by gantt collection setAddedProjects([]); setEditedProjects([]); setSelectedTeam(undefined); setSelectedProject(undefined); - }; + }, []); - const onAddNewSubtask = (parent: GanttTask) => { + const onAddNewSubtask = useCallback((parent: GanttTask) => { if (isProjectPreview(parent.element)) { setSelectedProject(parent.element); setShowSelectionModal(true); } - }; + }, []); - const onAddNewTask = (collection: GanttCollection) => { + const onAddNewTask = useCallback((collection: GanttCollection) => { setSelectedTeam(collection.element); setShowAddProjectModal(true); - }; + }, []); const handleAddWorkPackageInfo = ( workPackageInfo: { name: string; stage?: WorkPackageStage }, @@ -308,13 +336,13 @@ const ProjectGanttChartPage: FC = () => { return existingCarProjects + 1; }; - const handleWorkPackageSelected = () => { + const handleWorkPackageSelected = useCallback(() => { setShowAddWorkPackageModal(true); - }; + }, []); - const handleTaskSelected = () => { + const handleTaskSelected = useCallback(() => { setShowAddTaskModal(true); - }; + }, []); const handleAddTaskInfo = ( taskInfo: { @@ -406,20 +434,23 @@ const ProjectGanttChartPage: FC = () => { setGanttChanges([...ganttChanges, change]); }; - const createChangeHandler = (change: GanttChange) => { - const parentProject = allProjects.find((project) => wbsPipe(project.wbsNum) === projectWbsPipe(change.element.wbsNum)); // Find the project that either the change is on, or the changes work package is a part of - if (!parentProject) return; + const createChangeHandler = useCallback( + (change: GanttChange) => { + const parentProject = allProjects.find((project) => wbsPipe(project.wbsNum) === projectWbsPipe(change.element.wbsNum)); // Find the project that either the change is on, or the changes work package is a part of + if (!parentProject) return; - const { updatedProject } = applyChangesToWBSElement([change], change.element, parentProject); - const addedProject = addedProjects.find((proj) => proj.id === updatedProject.id); - if (addedProject) { - setAddedProjects((prev) => [...prev.filter((project) => project.id !== updatedProject.id), updatedProject]); - } else { - setEditedProjects((prev) => [...prev.filter((project) => project.id !== updatedProject.id), updatedProject]); - } + const { updatedProject } = applyChangesToWBSElement([change], change.element, parentProject); + const addedProject = addedProjects.find((proj) => proj.id === updatedProject.id); + if (addedProject) { + setAddedProjects((prev) => [...prev.filter((project) => project.id !== updatedProject.id), updatedProject]); + } else { + setEditedProjects((prev) => [...prev.filter((project) => project.id !== updatedProject.id), updatedProject]); + } - createChange(change); - }; + createChange(change); + }, + [allProjects, addedProjects] + ); const saveChanges = async () => { try { @@ -449,7 +480,7 @@ const ProjectGanttChartPage: FC = () => { toast.error('No Team Selected'); } }} - cars={allCars} + cars={cars} /> ); }; @@ -569,19 +600,19 @@ const ProjectGanttChartPage: FC = () => { } }; - const highlightProjectComparator = ( - highlightedElement: WbsElementPreview | Task, - wbsElement: WbsElementPreview | Task - ) => { - return projectWbsPipe(highlightedElement.wbsNum) === projectWbsPipe(wbsElement.wbsNum); - }; + const highlightProjectComparator = useCallback( + (highlightedElement: WbsElementPreview | Task, wbsElement: WbsElementPreview | Task) => { + return projectWbsPipe(highlightedElement.wbsNum) === projectWbsPipe(wbsElement.wbsNum); + }, + [] + ); - const highlightWorkPackageComparator = ( - highlightedElement: WbsElementPreview | Task, - wbsElement: WbsElementPreview | Task - ) => { - return wbsPipe(highlightedElement.wbsNum) === wbsPipe(wbsElement.wbsNum); - }; + const highlightWorkPackageComparator = useCallback( + (highlightedElement: WbsElementPreview | Task, wbsElement: WbsElementPreview | Task) => { + return wbsPipe(highlightedElement.wbsNum) === wbsPipe(wbsElement.wbsNum); + }, + [] + ); /* **************************************************** */ @@ -613,6 +644,22 @@ const ProjectGanttChartPage: FC = () => { ) : add(Date.now(), { weeks: 15 }); + const collapseHandler = () => { + allProjects.forEach((project) => { + setShowWorkPackagesMap((prev) => new Map(prev.set(project.id, false))); + }); + }; + + const expandHandler = () => { + allProjects.forEach((project) => { + setShowWorkPackagesMap((prev) => new Map(prev.set(project.id, true))); + }); + }; + + const toggleElementShowChildren = useCallback((element: WbsElementPreview | Task) => { + setShowWorkPackagesMap((prev) => new Map(prev.set(getElementId(element), !prev.get(getElementId(element))))); + }, []); + const headerRight = ( @@ -620,6 +667,8 @@ const ProjectGanttChartPage: FC = () => { carHandlers={carHandlers} teamTypeHandlers={teamTypeHandlers} teamHandlers={teamHandlers} + overdueHandler={overdueHandler} + hideTasksHandler={hideTasksHandler} resetHandler={resetHandler} /> diff --git a/src/frontend/src/pages/GuestDivisionPage/GuestSubteamCard.tsx b/src/frontend/src/pages/GuestDivisionPage/GuestSubteamCard.tsx deleted file mode 100644 index 6d4dae22b2..0000000000 --- a/src/frontend/src/pages/GuestDivisionPage/GuestSubteamCard.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Box, Card, CardContent, Stack, Typography, useTheme, Link } from '@mui/material'; -import { TeamPreview } from 'shared'; -import { NERButton } from '../../components/NERButton'; -import { Link as RouterLink } from 'react-router-dom'; - -interface GuestSubteamCardProps { - team: TeamPreview; -} - -const GuestSubteamCard: React.FC = ({ team }) => { - const theme = useTheme(); - - return ( - - - - - - - {team.teamName} - - - - Project Lead:{' '} - {team.head?.firstName && team.head?.lastName ? `${team.head.firstName} ${team.head.lastName}` : 'N/A'} - {' • '} - {team.leads.length} {team.leads.length === 1 ? 'lead' : 'leads'} - {' • '} - {team.members.length} {team.members.length === 1 ? 'member' : 'members'} - - - - - {team.description} - - - - Learn more - - - - - ); -}; - -export default GuestSubteamCard; diff --git a/src/frontend/src/pages/GuestDivisionPage/GuestTeamPage.tsx b/src/frontend/src/pages/GuestDivisionPage/GuestTeamPage.tsx deleted file mode 100644 index c9ab728c03..0000000000 --- a/src/frontend/src/pages/GuestDivisionPage/GuestTeamPage.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; -import { Box, useMediaQuery } from '@mui/system'; -import PageLayout from '../../components/PageLayout'; -import GuestSubteamCard from './GuestSubteamCard'; -import { useAllTeams } from '../../hooks/teams.hooks'; -import { useAllTeamTypes } from '../../hooks/team-types.hooks'; -import { Typography } from '@mui/material'; - -interface GuestTeamPageProps { - teamTypeId: string; -} - -const GuestTeamPage: React.FC = ({ teamTypeId }) => { - const isMobilePortrait = useMediaQuery('(max-width:480px)'); - const { isLoading: teamsIsLoading, isError: teamsIsError, data: allTeams, error: teamsError } = useAllTeams(); - const { - isLoading: teamTypesIsLoading, - isError: teamTypesIsError, - data: allTeamTypes, - error: teamTypesError - } = useAllTeamTypes(); - - if (teamsIsError) return ; - if (teamTypesIsError) return ; - if (teamsIsLoading || !allTeams || teamTypesIsLoading || !allTeamTypes) return ; - - const teams = allTeams.filter((team) => team.teamType?.teamTypeId === teamTypeId); - const teamTypeName = allTeamTypes.find((tt) => tt.teamTypeId === teamTypeId)?.name ?? ''; - - if (teams.length === 0) { - return ( - - - No Teams found for this Division - - - ); - } - - return ( - - - {teams.map((team) => ( - - ))} - - - ); -}; - -export default GuestTeamPage; diff --git a/src/frontend/src/pages/GuestEventPage/GuestEventCard.tsx b/src/frontend/src/pages/GuestEventPage/GuestEventCard.tsx deleted file mode 100644 index 291194aff1..0000000000 --- a/src/frontend/src/pages/GuestEventPage/GuestEventCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { alpha, Box, Card, CardContent, Chip, Stack, Typography, useTheme } from '@mui/material'; -import { EventInstance, formatEventDate, formatEventTime } from 'shared'; -import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; - -interface GuestEventCardProps { - event: EventInstance; -} - -const GuestEventCard: React.FC = ({ event }) => { - const theme = useTheme(); - - const displayDate = new Date(event.startTime); - const formattedDate = formatEventDate(displayDate); - const formattedTime = formatEventTime(displayDate); - - const wbsLabels = event.workPackages.map( - (wp) => - `${wp.wbsElement.carNumber}.${wp.wbsElement.projectNumber}.${wp.wbsElement.workPackageNumber} - ${wp.wbsElement.name}` - ); - - const title = wbsLabels.length > 0 ? wbsLabels[0] : event.title; - const extraWbs = wbsLabels.slice(1); - - return ( - - - - - - {title} - - {extraWbs.map((label) => ( - - {label} - - ))} - - - - - - - {formattedDate} @ {formattedTime} - - - - - {event.teams.length > 0 && ( - - {event.teams.map((team) => ( - - ))} - - )} - - {event.description && ( - - {event.description} - - )} - - - - ); -}; - -export default GuestEventCard; diff --git a/src/frontend/src/pages/GuestEventPage/GuestEventPage.tsx b/src/frontend/src/pages/GuestEventPage/GuestEventPage.tsx deleted file mode 100644 index 8cb7a23096..0000000000 --- a/src/frontend/src/pages/GuestEventPage/GuestEventPage.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Box, Button } from '@mui/material'; -import { Collapse, IconButton, Stack, Typography, useTheme } from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; -import GuestEventCard from './GuestEventCard'; -import { EventInstance, formatEventDate } from 'shared'; -import { useAllEventsPaginated } from '../../hooks/calendar.hooks'; - -const groupInstancesByDate = (instances: EventInstance[]): [string, EventInstance[]][] => { - const groups = new Map(); - for (const instance of instances) { - const date = new Date(instance.startTime); - const key = formatEventDate(date); - if (!groups.has(key)) groups.set(key, { date, instances: [] }); - groups.get(key)!.instances.push(instance); - } - return Array.from(groups.entries()) - .sort(([, a], [, b]) => b.date.getTime() - a.date.getTime()) - .map(([key, { instances }]) => [key, instances]); -}; - -interface DateGroupProps { - date: string; - instances: EventInstance[]; -} - -const DateGroup: React.FC = ({ date, instances }) => { - const theme = useTheme(); - const [open, setOpen] = useState(true); - - return ( - - setOpen((prev) => !prev)} - > - - {date} - - {open ? : } - - - - {instances.map((instance) => ( - - ))} - - - - ); -}; - -const GuestEventPage: React.FC = () => { - const [cursor, setCursor] = useState(undefined); - const [allInstances, setAllInstances] = useState([]); - const { data, isLoading, isError, error } = useAllEventsPaginated(cursor); - - useEffect(() => { - if (data?.instances) { - setAllInstances((prev) => { - const existingKeys = new Set(prev.map((i) => `${i.eventId}-${i.scheduleSlotId}`)); - const newInstances = data.instances.filter((i) => !existingKeys.has(`${i.eventId}-${i.scheduleSlotId}`)); - return newInstances.length > 0 ? [...prev, ...newInstances] : prev; - }); - } - }, [data]); - - if (isLoading && allInstances.length === 0) return ; - if (isError) return ; - - const groups = groupInstancesByDate(allInstances); - - return ( - - {groups.map(([date, instances]) => ( - - ))} - {data?.nextCursor && ( - - )} - - ); -}; - -export default GuestEventPage; diff --git a/src/frontend/src/pages/GuestProjectsPage/GuestProjectsCard.tsx b/src/frontend/src/pages/GuestProjectsPage/GuestProjectsCard.tsx deleted file mode 100644 index b6f43481e4..0000000000 --- a/src/frontend/src/pages/GuestProjectsPage/GuestProjectsCard.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { alpha, Box, Card, CardContent, Chip, Stack, Typography, useTheme, Link } from '@mui/material'; -import { wbsNamePipe, ProjectPreview, wbsPipe, WbsElementStatus } from 'shared'; -import { datePipe } from '../../utils/pipes'; -import { NERButton } from '../../components/NERButton'; -import { useSingleProject } from '../../hooks/projects.hooks'; -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; -import { Link as RouterLink } from 'react-router-dom'; - -interface ProjectCardProps { - project: ProjectPreview; -} - -const GuestProjectsCard: React.FC = ({ project }) => { - const theme = useTheme(); - const { data: singleProject, isLoading, isError, error } = useSingleProject(project.wbsNum); - if (isError) return ; - if (isLoading || !singleProject) return ; - - const activeWorkPackages = project.workPackages.filter((wp) => wp.status === WbsElementStatus.Active); - - return ( - - - - - - - {wbsNamePipe(singleProject)} - - {activeWorkPackages[0]?.stage ? ( - - ) : null} - - - Project Lead:{' '} - {singleProject.lead?.firstName && singleProject.lead?.lastName - ? `${singleProject.lead.firstName} ${singleProject.lead.lastName}` - : 'N/A'} - {' • '} - Project Manager:{' '} - {singleProject.manager?.firstName && singleProject.manager?.lastName - ? `${singleProject.manager.firstName} ${singleProject.manager.lastName}` - : 'N/A'} - - - {datePipe(singleProject.startDate) + - ' ⟝ ' + - singleProject.duration + - ' wks ⟞ ' + - datePipe(singleProject.endDate)} - - - - - {singleProject.summary} - - - - Learn more - - - - - ); -}; - -export default GuestProjectsCard; diff --git a/src/frontend/src/pages/GuestProjectsPage/GuestProjectsPage.tsx b/src/frontend/src/pages/GuestProjectsPage/GuestProjectsPage.tsx deleted file mode 100644 index c3b6f1587a..0000000000 --- a/src/frontend/src/pages/GuestProjectsPage/GuestProjectsPage.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useAllProjects } from '../../hooks/projects.hooks'; -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; -import { Box, useMediaQuery } from '@mui/system'; -import { wbsPipe } from 'shared'; -import PageLayout from '../../components/PageLayout'; -import GuestProjectsCard from './GuestProjectsCard'; -import { useAllTeamTypes } from '../../hooks/team-types.hooks'; -import { Chip } from '@mui/material'; -import { useState } from 'react'; - -const GuestProjectsPage: React.FC = () => { - const { data: allProjects, isLoading, isError, error } = useAllProjects(); - const [selectedTeamTypes, setSelectedTeamTypes] = useState([]); - const isMobilePortrait = useMediaQuery('(max-width:480px)'); - const { - isLoading: teamTypesIsLoading, - isError: teamTypesIsError, - data: teamTypes, - error: teamTypesError - } = useAllTeamTypes(); - - if (isLoading || !allProjects || teamTypesIsLoading || !teamTypes) return ; - if (isError) return ; - if (teamTypesIsError) return ; - - const filteredProjects = allProjects.filter( - (project) => - selectedTeamTypes.length === 0 || project.teamTypes.some((t) => t !== null && selectedTeamTypes.includes(t.name)) - ); - - return ( - - - {teamTypes.map((team) => ( - - setSelectedTeamTypes((prev) => - prev?.includes(team.name) ? prev.filter((t: string) => t !== team.name) : [...(prev || []), team.name] - ) - } - clickable - color={selectedTeamTypes?.includes(team.name) ? 'primary' : 'default'} - sx={{ flexShrink: 0 }} - /> - ))} - - - {filteredProjects.map((p) => ( - - ))} - - - ); -}; - -export default GuestProjectsPage; diff --git a/src/frontend/src/pages/HomePage/components/FeaturedProjects.tsx b/src/frontend/src/pages/HomePage/components/FeaturedProjects.tsx index 8b930d5017..1b25ed884d 100644 --- a/src/frontend/src/pages/HomePage/components/FeaturedProjects.tsx +++ b/src/frontend/src/pages/HomePage/components/FeaturedProjects.tsx @@ -3,14 +3,15 @@ * See the LICENSE file in the repository root folder for details. */ +import FeaturedProjectsCard from './FeaturedProjectsCard'; import { useFeaturedProjects } from '../../../hooks/organizations.hooks'; import ErrorPage from '../../ErrorPage'; +import { wbsPipe } from 'shared'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import { Box, Stack, useMediaQuery } from '@mui/material'; import { Error } from '@mui/icons-material'; -import GuestProjectsCard from '../../GuestProjectsPage/GuestProjectsCard'; const NoFeaturedProjectsDisplay: React.FC = () => { return ( @@ -50,11 +51,7 @@ const FeaturedProjects: React.FC = () => { {featuredProjects.length === 0 ? ( ) : ( - featuredProjects.map((p) => ( - - - - )) + featuredProjects.map((p) => ) )} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectCreateContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectCreateContainer.tsx index 929370923c..9cad6c1605 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectCreateContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectCreateContainer.tsx @@ -23,7 +23,7 @@ import { ProjectCreateChangeRequestFormInput } from './ProjectEditContainer'; import { dateToMidnightUTC, ProjectProposedChangesCreateArgs, WbsNumber, WorkPackageStage } from 'shared'; import { CreateStandardChangeRequestPayload, useCreateStandardChangeRequest } from '../../../hooks/change-requests.hooks'; import { useCreateSingleWorkPackage } from '../../../hooks/work-packages.hooks'; -import { useGlobalCarFilter } from '../../../app/AppGlobalCarFilterContext'; +import { useGetAllCars } from '../../../hooks/cars.hooks'; import { ChangeRequestReason } from 'shared'; import { yupResolver } from '@hookform/resolvers/yup'; import { ChangeRequestType } from 'shared'; @@ -35,7 +35,8 @@ const ProjectCreateContainer: React.FC = () => { const [managerId, setManagerId] = useState(); const [leadId, setLeadId] = useState(); - const { selectedCar, isLoading: carFilterIsLoading } = useGlobalCarFilter(); + const [carNumber, setCarNumber] = useState(); + const { data: cars, isLoading: carsIsLoading, isError: carsIsError, error: carsError } = useGetAllCars(); const { mutateAsync: createProjectMutateAsync, isLoading: createProjectIsLoading } = useCreateSingleProject(); const { mutateAsync: mutateCRAsync, isLoading: isCRHookLoading } = useCreateStandardChangeRequest(); @@ -46,7 +47,7 @@ const ProjectCreateContainer: React.FC = () => { budget: 0, summary: '', teamIds: [], - carNumber: selectedCar === 'all-cars' ? undefined : selectedCar.wbsNum.carNumber, + carNumber, links: [], crId: query.get('crId') || undefined, descriptionBullets: [], @@ -132,11 +133,14 @@ const ProjectCreateContainer: React.FC = () => { createWpIsLoading || !allLinkTypes || allLinkTypesIsLoading || - carFilterIsLoading + carsIsLoading || + !cars ) return ; if (allLinkTypesIsError) return ; + if (carsIsError) return ; + const requiredLinkTypeNames = getRequiredLinkTypeNames(allLinkTypes); const onSubmitChangeRequest = async (data: ProjectCreateChangeRequestFormInput) => { @@ -276,6 +280,7 @@ const ProjectCreateContainer: React.FC = () => { leadId={leadId} managerId={managerId} onSubmitChangeRequest={onSubmitChangeRequest} + setCarNumber={setCarNumber} changeRequestFormReturn={changeRequestFormMethods} /> ); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx index 65c1c43731..b1034e9ff8 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx @@ -45,6 +45,7 @@ const ProjectEditContainer: React.FC = ({ project, ex const { name, budget, summary, workPackages } = project; const [managerId, setManagerId] = useState(project.manager?.userId.toString()); const [leadId, setLeadId] = useState(project.lead?.userId.toString()); + const [carNumber, setCarNumber] = useState(project.wbsNum.carNumber); const descriptionBullets = bulletsToObject(project.descriptionBullets); const { mutateAsync, isLoading } = useEditSingleProject(project.wbsNum); @@ -140,7 +141,7 @@ const ProjectEditContainer: React.FC = ({ project, ex summary, // teamId and carNumber aren't used for projectEdit teamIds: [], - carNumber: project.wbsNum.carNumber, + carNumber, links, crId: query.get('crId') || '', descriptionBullets, @@ -289,6 +290,7 @@ const ProjectEditContainer: React.FC = ({ project, ex leadId={leadId} managerId={managerId} onSubmitChangeRequest={onSubmitChangeRequest} + setCarNumber={setCarNumber} onlyLeadershipChanged={onlyLeadershipChanged} /> ); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx index 8829498789..dfcfc9fd2b 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx @@ -56,6 +56,8 @@ interface ProjectFormContainerProps { leadId?: string; managerId?: string; onSubmitChangeRequest?: (data: ProjectCreateChangeRequestFormInput) => void; + setCarNumber: (carNumber: number) => void; + carNumber?: number; changeRequestFormReturn: ChangeRequestFormReturn; onlyLeadershipChanged?: boolean; } @@ -71,6 +73,7 @@ const ProjectFormContainer: React.FC = ({ leadId, managerId, onSubmitChangeRequest, + setCarNumber, changeRequestFormReturn, onlyLeadershipChanged }) => { @@ -304,6 +307,7 @@ const ProjectFormContainer: React.FC = ({ leadId={leadId} managerId={managerId} project={project} + setCarNumber={setCarNumber} /> diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectFormDetails.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectFormDetails.tsx index c5c5e63691..4aa5f88d2b 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectFormDetails.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectFormDetails.tsx @@ -1,5 +1,5 @@ import { Project, User } from 'shared'; -import { Box, FormControl, FormHelperText, FormLabel, Grid, MenuItem, TextField, Typography } from '@mui/material'; +import { Box, FormControl, FormLabel, Grid, MenuItem, Select, Typography } from '@mui/material'; import ReactHookTextField from '../../../components/ReactHookTextField'; import { fullNamePipe } from '../../../utils/pipes'; import NERAutocomplete from '../../../components/NERAutocomplete'; @@ -8,8 +8,9 @@ import { Control, Controller, FieldErrorsImpl } from 'react-hook-form'; import { AttachMoney } from '@mui/icons-material'; import TeamDropdown from '../../../components/TeamsDropdown'; import ChangeRequestDropdown from '../../../components/ChangeRequestDropdown'; -import { useGlobalCarFilter } from '../../../app/AppGlobalCarFilterContext'; +import { useGetAllCars } from '../../../hooks/cars.hooks'; import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; interface ProjectEditDetailsProps { users: User[]; @@ -21,6 +22,7 @@ interface ProjectEditDetailsProps { setManagerId: (id?: string) => void; setLeadId: (id?: string) => void; setcrId?: (crId?: number) => void; + setCarNumber: (carNumber: number) => void; } const userToAutocompleteOption = (user?: User): { label: string; id: string } => { @@ -36,15 +38,18 @@ const ProjectFormDetails: React.FC = ({ managerId, leadId, setLeadId, - setManagerId + setManagerId, + setCarNumber }) => { - const { selectedCar, allCars, isLoading: carFilterIsLoading } = useGlobalCarFilter(); + const { data: cars, isLoading, isError, error } = useGetAllCars(); - if (carFilterIsLoading) { + if (isLoading || !cars) { return ; } - const sortedCars = [...allCars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber); + if (isError) { + return ; + } return ( @@ -63,33 +68,43 @@ const ProjectFormDetails: React.FC = ({ /> - {!project && selectedCar === 'all-cars' && ( - - - Car - ( - - {sortedCars.map((car) => ( - - {car.name} - - ))} - - )} - /> - {errors.carNumber?.message} - - - )} {!project && ( - - - - - + <> + + + Car + ( + + )} + > + + + + + + + + )} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTable.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTable.tsx index 0becab27bd..fe7b8531b1 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTable.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTable.tsx @@ -63,8 +63,7 @@ const BOMTable: React.FC = ({ setHideColumn, assignMaterial, colu subtotal: '', link: '', notes: '', - assemblyId: assembly.assemblyId, - isCopied: false + assemblyId: assembly.assemblyId }); assemblyMaterials.forEach((material, indx) => materialsWithAssemblies.push(materialToRow(material, indx))); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx index 4345afcf96..c9afa898dc 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx @@ -11,8 +11,7 @@ import { useToast } from '../../../../hooks/toasts.hooks'; import { useAssignMaterialToAssembly, useDeleteAssembly, useDeleteMaterial } from '../../../../hooks/bom.hooks'; import LoadingIndicator from '../../../../components/LoadingIndicator'; import EditMaterialModal from './MaterialForm/EditMaterialModal'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { Button, Link, Tooltip, Typography } from '@mui/material'; +import { Button, Link, Typography } from '@mui/material'; import { bomBaseColDef } from '../../../../utils/bom.utils'; import NERModal from '../../../../components/NERModal'; import { renderStatusBOM } from './BOMTableCustomCells'; @@ -301,20 +300,7 @@ const BOMTableWrapper: React.FC = ({ type: 'string', sortable: false, filterable: false, - hide: hideColumn[3], - renderCell: (params) => { - const material = materials.find((m) => m.materialId === params.row.materialId); - return ( - - {params.value} - {material?.isCopied && ( - - - - )} - - ); - } + hide: hideColumn[3] }, { ...bomBaseColDef, diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/BOMCopyConfirmModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/BOMCopyConfirmModal.tsx deleted file mode 100644 index 5076bf890f..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/BOMCopyConfirmModal.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import NERModal from '../../../../../components/NERModal'; -import { useCopyMaterialsToProject } from '../../../../../hooks/bom.hooks'; - -export interface BOMCopyConfirmModalProps { - open: boolean; - onHide: () => void; - onSuccess: () => void; - materialIds: string[]; - sourceProjectName: string; - currentProjectName: string; - destinationWbsNum: string; -} - -const BOMCopyConfirmModal = ({ - open, - onHide, - onSuccess, - materialIds, - sourceProjectName, - currentProjectName, - destinationWbsNum -}: BOMCopyConfirmModalProps) => { - const { mutateAsync: copyMaterials } = useCopyMaterialsToProject(); - - const handleConfirm = async () => { - await copyMaterials({ materialIds, destinationWbsNum }); - onSuccess(); - onHide(); - }; - - const message = `Are you sure you want to copy ${materialIds.length} materials from ${sourceProjectName} to ${currentProjectName}?`; - return ( - -

{message}

-
- ); -}; - -export default BOMCopyConfirmModal; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMModal.tsx deleted file mode 100644 index 8f77fbcd23..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMModal.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { WbsNumber, wbsPipe } from 'shared'; -import CopyBOMView from './CopyBOMView'; -import { useGetAllCars } from '../../../../../hooks/cars.hooks'; -import { useAllProjects } from '../../../../../hooks/projects.hooks'; -import React, { useState } from 'react'; -import ErrorPage from '../../../../ErrorPage'; -import LoadingIndicator from '../../../../../components/LoadingIndicator'; -import BOMCopyConfirmModal from './BOMCopyConfirmModal'; - -export interface CopyBOMModalProps { - open: boolean; - onHide: () => void; - destinationWbsNum: WbsNumber; - currentProjectName: string; -} - -const CopyBOMModal: React.FC = ({ open, onHide, destinationWbsNum, currentProjectName }) => { - const { data: cars, isLoading: isLoadingCars, isError: carsIsError, error: carsError } = useGetAllCars(); - const { data: projects, isLoading: isLoadingProjects, isError: projectsIsError, error: projectsError } = useAllProjects(); - const [confirmOpen, setConfirmOpen] = useState(false); - const [confirmedMaterialIds, setConfirmedMaterialIds] = useState([]); - const [confirmedSourceProjectName, setConfirmedSourceProjectName] = useState(''); - - if (carsIsError) return ; - if (projectsIsError) return ; - if (isLoadingCars || !cars || isLoadingProjects || !projects) return ; - - const destinationWbs = wbsPipe(destinationWbsNum); - - return ( - <> - { - setConfirmedMaterialIds(materialIds); - setConfirmedSourceProjectName(sourceProjectName); - setConfirmOpen(true); - }} - /> - setConfirmOpen(false)} - onSuccess={() => { - onHide(); - setConfirmOpen(false); - }} - materialIds={confirmedMaterialIds} - sourceProjectName={confirmedSourceProjectName} - currentProjectName={`${wbsPipe(destinationWbsNum)} - ${currentProjectName}`} - destinationWbsNum={destinationWbs} - /> - - ); -}; - -export default CopyBOMModal; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMProjectSection.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMProjectSection.tsx deleted file mode 100644 index a53a4a9bf3..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMProjectSection.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useEffect } from 'react'; -import { Typography } from '@mui/material'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { useState } from 'react'; -import { ProjectPreview } from 'shared'; -import LoadingIndicator from '../../../../../components/LoadingIndicator'; -import { useGetAssembliesForWbsElement, useGetMaterialsForWbsElement } from '../../../../../hooks/bom.hooks'; -import ErrorPage from '../../../../ErrorPage'; - -interface CopyBOMProjectSectionProps { - selectedProject: ProjectPreview; - onSelectionChange: (materialIds: string[]) => void; -} - -const columns: GridColDef[] = [ - { field: 'materialName', headerName: 'Material Name', flex: 1 }, - { field: 'manufacturer', headerName: 'Manufacturer', flex: 1 }, - { field: 'materialType', headerName: 'Material Type', flex: 1 }, - { field: 'assembly', headerName: 'Assembly Name', flex: 1 } -]; - -const CopyBOMProjectSection: React.FC = ({ selectedProject, onSelectionChange }) => { - const [selectedMaterialIds, setSelectedMaterialIds] = useState([]); - const { - data: materials, - isLoading: isLoadingMaterials, - isError: isErrorMaterials, - error: materialsError - } = useGetMaterialsForWbsElement(selectedProject.wbsNum); - - const { - data: assemblies, - isLoading: isLoadingAssemblies, - isError: isErrorAssemblies, - error: assembliesError - } = useGetAssembliesForWbsElement(selectedProject.wbsNum); - - useEffect(() => { - if (materials) { - const allIds = materials.map((m) => m.materialId); - setSelectedMaterialIds(allIds); - onSelectionChange(allIds); - } - }, [materials, onSelectionChange]); - - if (isErrorMaterials) return ; - if (isErrorAssemblies) return ; - if (isLoadingMaterials || isLoadingAssemblies || !materials || !assemblies) return ; - - const rows = materials.map((m) => ({ - id: m.materialId, - materialName: m.name, - manufacturer: m.manufacturer?.name ?? '-', - materialType: m.materialType.name, - assembly: assemblies.find((a) => a.assemblyId === m.assemblyId)?.name ?? '-' - })); - - return ( - <> - - {selectedMaterialIds.length} material{selectedMaterialIds.length !== 1 ? 's' : ''} selected - - { - const ids = newModel as string[]; - setSelectedMaterialIds(ids); - onSelectionChange(ids); - }} - rowsPerPageOptions={[100]} - hideFooterPagination - sx={{ - '& .MuiDataGrid-columnHeaders': { backgroundColor: '#ef4345', color: 'white' }, - '& .MuiDataGrid-columnHeaders .MuiCheckbox-root': { color: 'white' } - }} - /> - - ); -}; - -export default CopyBOMProjectSection; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMView.tsx deleted file mode 100644 index 957500d57b..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/CopyBOMView.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useRef, useState } from 'react'; -import { Box, Grid } from '@mui/material'; -import { Car, ProjectPreview, wbsPipe } from 'shared'; -import NERModal from '../../../../../components/NERModal'; -import NERAutocomplete from '../../../../../components/NERAutocomplete'; -import CopyBOMProjectSection from './CopyBOMProjectSection'; - -interface CopyBOMViewProps { - open: boolean; - onHide: () => void; - cars: Car[]; - projects: ProjectPreview[]; - onCopy: (materialIds: string[], sourceProjectName: string) => void; -} - -const CopyBOMView: React.FC = ({ open, onHide, cars, projects, onCopy }) => { - const [selectedCar, setSelectedCar] = useState(null); - const [selectedProject, setSelectedProject] = useState(null); - const [hasSelection, setHasSelection] = useState(false); - const selectedMaterialIdsRef = useRef([]); - - const carOptions = cars.map((car) => ({ - label: `${car.wbsNum.carNumber} - ${car.name}`, - id: car.id - })); - - const filteredProjects = selectedCar - ? projects.filter((p) => p.wbsNum.carNumber === selectedCar.wbsNum.carNumber) - : projects; - - const projectOptions = filteredProjects.map((p) => ({ - label: `${wbsPipe(p.wbsNum)} - ${p.name}`, - id: wbsPipe(p.wbsNum) - })); - - const handleSubmit = async () => { - if (!selectedProject) return; - const sourceProjectName = `${wbsPipe(selectedProject.wbsNum)} - ${selectedProject.name}`; - onCopy(selectedMaterialIdsRef.current, sourceProjectName); - }; - - return ( - - - - { - const car = newValue ? (cars.find((c) => c.id === newValue.id) ?? null) : null; - setSelectedCar(car); - setSelectedProject(null); - }} - value={ - selectedCar ? { label: `${selectedCar.wbsNum.carNumber} - ${selectedCar.name}`, id: selectedCar.id } : null - } - placeholder="Select Car" - size="medium" - /> - - - { - const project = newValue ? (filteredProjects.find((p) => wbsPipe(p.wbsNum) === newValue.id) ?? null) : null; - setSelectedProject(project); - }} - value={ - selectedProject - ? { - label: `${wbsPipe(selectedProject.wbsNum)} - ${selectedProject.name}`, - id: wbsPipe(selectedProject.wbsNum) - } - : null - } - placeholder="Select Project" - size="medium" - disabled={!selectedCar} - /> - - - - {selectedProject ? ( - { - selectedMaterialIdsRef.current = ids; - setHasSelection(ids.length > 0); - }} - /> - ) : ( - - Select a project to view its materials - - )} - - - - ); -}; - -export default CopyBOMView; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/SelectMaterialToCopyModal.tsx deleted file mode 100644 index 94a26981f9..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/CopyBOM/SelectMaterialToCopyModal.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Autocomplete, InputAdornment, Stack, TextField, Typography } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import { useForm } from 'react-hook-form'; -import { Assembly, Car, Material, ProjectPreview, WbsNumber } from 'shared'; - -import NERFormModal from '../../../../../components/NERFormModal'; -import NERAutocomplete from '../../../../../components/NERAutocomplete'; -import LoadingIndicator from '../../../../../components/LoadingIndicator'; -import ErrorPage from '../../../../ErrorPage'; - -import { useGetAllCars } from '../../../../../hooks/cars.hooks'; -import { useAllProjects } from '../../../../../hooks/projects.hooks'; -import { useGetMaterialsForWbsElement, useGetMaterialsForCar } from '../../../../../hooks/bom.hooks'; - -type AutocompleteOption = { label: string; id: string }; - -interface SelectMaterialToCopyModalProps { - open: boolean; - onHide: () => void; - onSelect: (material: Material) => void; - assemblies: Assembly[]; -} - -type FormValues = Record; - -const carToOption = (car: Car): AutocompleteOption => ({ - label: `Car ${car.wbsNum.carNumber} - ${car.name}`, - id: car.wbsElementId -}); - -const projectToOption = (project: ProjectPreview): AutocompleteOption => ({ - label: `${project.wbsNum.carNumber}.${project.wbsNum.projectNumber} - ${project.name}`, - id: project.wbsElementId -}); - -const projectToWbsNumber = (project: ProjectPreview): WbsNumber => ({ - carNumber: project.wbsNum.carNumber, - projectNumber: project.wbsNum.projectNumber, - workPackageNumber: 0 -}); - -const getLatestCar = (cars: Car[]): Car | null => { - if (cars.length === 0) return null; - return [...cars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber)[0]; -}; - -const SelectMaterialToCopyModal: React.FC = ({ open, onHide, onSelect, assemblies }) => { - const { reset, handleSubmit } = useForm(); - - const [selectedCar, setSelectedCar] = useState(null); - const [selectedProject, setSelectedProject] = useState(null); - const [selectedMaterial, setSelectedMaterial] = useState(null); - - const { data: cars, isLoading: carsIsLoading, isError: carsIsError, error: carsError } = useGetAllCars(); - - const { data: projects, isLoading: projectsIsLoading, isError: projectsIsError, error: projectsError } = useAllProjects(); - - const allCars = useMemo(() => cars ?? [], [cars]); - const allProjects = useMemo(() => projects ?? [], [projects]); - - const latestCar = useMemo(() => getLatestCar(allCars), [allCars]); - const effectiveCar = selectedCar ?? latestCar; - - const projectsForSelectedCar = useMemo(() => { - if (!effectiveCar) return []; - return allProjects.filter((p) => p.wbsNum.carNumber === effectiveCar.wbsNum.carNumber); - }, [allProjects, effectiveCar]); - - const selectedProjectWbsNum = useMemo( - () => (selectedProject ? projectToWbsNumber(selectedProject) : null), - [selectedProject] - ); - - // Materials for the selected project for autocomplete - const { - data: projectMaterials, - isLoading: projectMaterialsIsLoading, - isError: projectMaterialsIsError, - error: projectMaterialsError - } = useGetMaterialsForWbsElement(selectedProjectWbsNum ?? { carNumber: 0, projectNumber: 0, workPackageNumber: 0 }); - - // All materials across the selected car for search bar - const { - data: carMaterials, - isLoading: carMaterialsIsLoading, - isError: carMaterialsIsError, - error: carMaterialsError - } = useGetMaterialsForCar(effectiveCar?.wbsNum.carNumber ?? null, allProjects); - - const assemblyNameById = useMemo(() => new Map(assemblies.map((a) => [a.assemblyId, a.name])), [assemblies]); - - const materialToOption = useCallback( - (material: Material): AutocompleteOption => ({ - label: [ - material.name, - material.manufacturerName, - material.materialTypeName, - material.assemblyId ? `Assembly: ${assemblyNameById.get(material.assemblyId) ?? material.assemblyId}` : undefined - ] - .filter(Boolean) - .join(' – '), - id: material.materialId - }), - [assemblyNameById] - ); - - const materials = useMemo(() => (selectedProject ? (projectMaterials ?? []) : []), [selectedProject, projectMaterials]); - - const carOptions = useMemo(() => allCars.map(carToOption), [allCars]); - const projectOptions = useMemo(() => projectsForSelectedCar.map(projectToOption), [projectsForSelectedCar]); - const materialOptions = useMemo(() => materials.map(materialToOption), [materials, materialToOption]); - const carMaterialOptions = useMemo(() => (carMaterials ?? []).map(materialToOption), [carMaterials, materialToOption]); - - const selectedCarOption = effectiveCar ? (carOptions.find((o) => o.id === effectiveCar.wbsElementId) ?? null) : null; - const selectedProjectOption = selectedProject ? projectToOption(selectedProject) : null; - const selectedMaterialOption = selectedMaterial ? materialToOption(selectedMaterial) : null; - - // Selecting from the search bar auto-populates the project and material dropdowns - const handleSearchSelect = useCallback( - (_, value: AutocompleteOption | null) => { - if (!value) return; - const material = (carMaterials ?? []).find((m) => m.materialId === value.id) ?? null; - if (!material) return; - const project = allProjects.find((p) => p.wbsElementId === material.wbsElementId) ?? null; - setSelectedProject(project); - setSelectedMaterial(material); - }, - [carMaterials, allProjects] - ); - - const handleCarChange = useCallback( - (_: React.SyntheticEvent, value: AutocompleteOption | null) => { - const next = value ? (allCars.find((c) => c.wbsElementId === value.id) ?? null) : null; - setSelectedCar(next); - setSelectedProject(null); - setSelectedMaterial(null); - }, - [allCars] - ); - - const handleProjectChange = useCallback( - (_: React.SyntheticEvent, value: AutocompleteOption | null) => { - const next = value ? (projectsForSelectedCar.find((p) => p.wbsElementId === value.id) ?? null) : null; - setSelectedProject(next); - setSelectedMaterial(null); - }, - [projectsForSelectedCar] - ); - - const handleMaterialChange = useCallback( - (_: React.SyntheticEvent, value: AutocompleteOption | null) => { - const next = value ? (materials.find((m) => m.materialId === value.id) ?? null) : null; - setSelectedMaterial(next); - }, - [materials] - ); - - const handleCopy = () => { - if (!selectedMaterial) return; - onSelect(selectedMaterial); - onHide(); - reset(); - setSelectedCar(null); - setSelectedProject(null); - setSelectedMaterial(null); - }; - - const handleHide = () => { - onHide(); - reset(); - setSelectedCar(null); - setSelectedProject(null); - setSelectedMaterial(null); - }; - - const modalContent = () => { - if (carsIsError) return ; - if (projectsIsError) return ; - if (projectMaterialsIsError) return ; - if (carMaterialsIsError) return ; - if (carsIsLoading || projectsIsLoading) return ; - - return ( - - option.label} - onChange={handleSearchSelect} - disabled={!effectiveCar || carMaterialsIsLoading} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - - - - - - - - {!selectedMaterial && ( - - Pick a material to enable "Copy". - - )} - - ); - }; - - return ( - - {modalContent()} - - ); -}; - -export default SelectMaterialToCopyModal; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 5eb4db39b2..5520cccd97 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -2,7 +2,6 @@ import { Accordion, AccordionDetails, AccordionSummary, - Button, FormControl, FormHelperText, FormLabel, @@ -15,7 +14,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue, UseFormWatch } from 'react-hook-form'; -import { Assembly, Manufacturer, Material, MaterialType, Unit } from 'shared'; +import { Assembly, Manufacturer, MaterialType, Unit } from 'shared'; import ReactHookTextField from '../../../../../components/ReactHookTextField'; import { MaterialFormInput } from './MaterialForm'; import NERFormModal from '../../../../../components/NERFormModal'; @@ -27,7 +26,6 @@ import { displayEnum } from '../../../../../utils/pipes'; import { MaterialStatus } from 'shared'; import React, { useState } from 'react'; import { AddCircle } from '@mui/icons-material'; -import SelectMaterialToCopyModal from '../CopyBOM/SelectMaterialToCopyModal'; export interface MaterialFormViewProps { submitText: 'Add' | 'Edit'; @@ -44,6 +42,7 @@ export interface MaterialFormViewProps { watch: UseFormWatch; createManufacturer: (name: string) => void; setValue: UseFormSetValue; + copyFromExistingBomAction?: React.ReactNode; fromRRForm?: boolean; } @@ -78,23 +77,6 @@ const MaterialFormView: React.FC = ({ const price = watch('price'); const subtotal = quantity && price ? quantity * price : 0; - const [copyModalOpen, setCopyModalOpen] = React.useState(false); - - const handleCopySelect = (m: Material) => { - setValue('name', m.name ?? ''); - setValue('materialTypeName', m.materialTypeName ?? ''); - setValue('manufacturerName', m.manufacturerName ?? ''); - setValue('manufacturerPartNumber', m.manufacturerPartNumber ?? ''); - setValue('pdmFileName', m.pdmFileName ?? ''); - setValue('linkUrl', m.linkUrl ?? ''); - setValue('quantity', m.quantity != null ? Number(m.quantity) : undefined); - setValue('unitName', m.unitName ?? undefined); - setValue('price', m.price != null ? m.price / 100 : undefined); - setValue('notes', m.notes ?? ''); - setValue('assemblyId', undefined); - - setCopyModalOpen(false); - }; const optionalFields = ( @@ -522,7 +504,7 @@ const MaterialFormView: React.FC = ({ )} - {submitText === 'Add' && ( + {/*submitText === 'Add' && ( = ({ - )} - setCopyModalOpen(false)} - onSelect={handleCopySelect} - assemblies={assemblies ?? []} - /> + )*/}
); }; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOMTab.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOMTab.tsx index 736e27aa04..e3eba4e447 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOMTab.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOMTab.tsx @@ -2,12 +2,11 @@ import { Box } from '@mui/system'; import { MaterialPreview, Project, isGuest } from 'shared'; import { NERButton } from '../../../components/NERButton'; import WarningIcon from '@mui/icons-material/Warning'; -import React, { useState } from 'react'; import { Tooltip, useTheme } from '@mui/material'; +import { useState } from 'react'; import BOMTableWrapper from './BOM/BOMTableWrapper'; import CreateMaterialModal from './BOM/MaterialForm/CreateMaterialModal'; import CreateAssemblyModal from './BOM/AssemblyForm/CreateAssemblyModal'; -import CopyBOMModal from './BOM/CopyBOM/CopyBOMModal'; import NERSuccessButton from '../../../components/NERSuccessButton'; import { centsToDollar } from '../../../utils/pipes'; import { useCurrentUser } from '../../../hooks/users.hooks'; @@ -29,10 +28,9 @@ const BOMTab = ({ project }: { project: Project }) => { const [hideColumn, setHideColumn] = useState(initialHideColumn); const [showAddMaterial, setShowAddMaterial] = useState(false); const [showAddAssembly, setShowAddAssembly] = useState(false); - const [showCopyBOM, setShowCopyBOM] = useState(false); const [showImportBOM, setShowImportBOM] = useState(false); - const theme = useTheme(); + const user = useCurrentUser(); const { @@ -99,12 +97,6 @@ const BOMTab = ({ project }: { project: Project }) => { allUnits={units} assemblies={assemblies} /> - setShowCopyBOM(false)} - destinationWbsNum={project.wbsNum} - currentProjectName={project.name} - /> { > Show All Columns - setShowCopyBOM(true)} disabled={isGuest(user.role)}> + {/* + {}} disabled={isGuest(user.role)}> Copy Existing BOM + */} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx index bd2ed987ba..1a13260a18 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx @@ -4,7 +4,7 @@ */ import { Link, useHistory } from 'react-router-dom'; -import { Project, isGuest, isAdmin, isLeadership, RoleEnum } from 'shared'; +import { Project, isGuest, isAdmin, isLeadership } from 'shared'; import { projectWbsPipe, wbsPipe } from '../../../utils/pipes'; import ProjectDetails from './ProjectDetails'; import { routes } from '../../../utils/routes'; @@ -207,7 +207,7 @@ const ProjectViewContainer: React.FC = ({ project, en {tab === 0 ? ( ) : tab === 1 ? ( - + ) : tab === 2 ? ( ) : tab === 3 ? ( diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/GuestTasksList.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/GuestTasksList.tsx deleted file mode 100644 index 01bdedfa18..0000000000 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/GuestTasksList.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Typography } from '@mui/material'; -import { Box, useTheme } from '@mui/system'; -import { Project, TaskPriority, TaskStatus } from 'shared'; - -const TaskCard = ({ task }: { task: any }) => { - const theme = useTheme(); - const getPriorityColor = (priority: TaskPriority) => { - switch (priority) { - case TaskPriority.High: - return '#ff0000'; - case TaskPriority.Medium: - return '#ff9800'; - default: - return '#4caf50'; - } - }; - - return ( - - - {task.priority} - - - {task.title} - - ); -}; - -export const GuestsTasksList = ({ project }: { project: Project }) => { - const backLogTasks = project.tasks.filter((task) => task.status === TaskStatus.IN_BACKLOG); - const inProgressTasks = project.tasks.filter((task) => task.status === TaskStatus.IN_PROGRESS); - const doneTasks = project.tasks.filter((task) => task.status === TaskStatus.DONE); - - return ( - - {backLogTasks.length === 0 && inProgressTasks.length === 0 && doneTasks.length === 0 ? ( - - This project has no tasks associated with it - - ) : ( - <> - {backLogTasks.length > 0 && ( - <> - - Back Log - - {backLogTasks.map((task) => ( - - ))} - - )} - {inProgressTasks.length > 0 && ( - <> - - In Progress - - {inProgressTasks.map((task) => ( - - ))} - - )} - {doneTasks.length > 0 && ( - <> - - Done - - {doneTasks.map((task) => ( - - ))} - - )} - - )} - - ); -}; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/TaskFormModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/TaskFormModal.tsx index 762123f7db..bbf2ee83bc 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/TaskFormModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/TaskFormModal.tsx @@ -2,7 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { Autocomplete, FormControl, FormHelperText, FormLabel, Grid, MenuItem, TextField } from '@mui/material'; import { DatePicker } from '@mui/x-date-pickers'; import { Controller, useForm } from 'react-hook-form'; -import { countWords, isGuest, isUnderWordCount, Task, TaskPriority, TaskStatus, TeamPreview } from 'shared'; +import { countWords, isGuest, isUnderWordCount, Task, TaskPriority, TeamPreview } from 'shared'; import { useAllMembers, useCurrentUser } from '../../../../hooks/users.hooks'; import * as yup from 'yup'; import { taskUserToAutocompleteOption } from '../../../../utils/task.utils'; @@ -10,6 +10,16 @@ import NERFormModal from '../../../../components/NERFormModal'; import LoadingIndicator from '../../../../components/LoadingIndicator'; import ErrorPage from '../../../ErrorPage'; +const schema = yup.object().shape({ + notes: yup.string().optional(), + startDate: yup.date().optional(), + deadline: yup.date().optional(), + priority: yup.mixed().oneOf(Object.values(TaskPriority)).required(), + assignees: yup.array().required(), + title: yup.string().required(), + taskId: yup.string().required() +}); + export interface EditTaskFormInput { taskId: string; title: string; @@ -22,7 +32,6 @@ export interface EditTaskFormInput { interface TaskFormModalProps { task?: Task; - status?: Task['status']; teams: TeamPreview[]; modalShow: boolean; onHide: () => void; @@ -30,45 +39,7 @@ interface TaskFormModalProps { onReset?: () => void; } -const TaskFormModal: React.FC = ({ task, status, onSubmit, modalShow, onHide, onReset }) => { - let schema; - - if (status === TaskStatus.IN_PROGRESS) { - schema = yup.object().shape({ - notes: yup - .string() - .optional() - .test((value) => { - if (!value) return true; - const wordCount = countWords(value); - return wordCount < 250; - }), - startDate: yup.date().optional(), - deadline: yup.date().required('Deadline is required for In Progress tasks'), - priority: yup.mixed().oneOf(Object.values(TaskPriority)).required(), - assignees: yup.array().required().min(1, 'At least one assignee is required for In Progress tasks'), - title: yup.string().required(), - taskId: yup.string().required() - }); - } else { - schema = yup.object().shape({ - notes: yup - .string() - .optional() - .test((value) => { - if (!value) return true; - const wordCount = countWords(value); - return wordCount < 250; - }), - startDate: yup.date().optional(), - deadline: yup.date().optional(), - priority: yup.mixed().oneOf(Object.values(TaskPriority)).required(), - assignees: yup.array().required(), - title: yup.string().required(), - taskId: yup.string().required() - }); - } - +const TaskFormModal: React.FC = ({ task, onSubmit, modalShow, onHide, onReset }) => { const user = useCurrentUser(); const { data: users, isLoading, isError, error } = useAllMembers(); @@ -191,7 +162,6 @@ const TaskFormModal: React.FC = ({ task, status, onSubmit, m /> )} /> - {errors.assignees?.message} @@ -230,7 +200,6 @@ const TaskFormModal: React.FC = ({ task, status, onSubmit, m /> )} /> - {errors.deadline?.message} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskCard.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskCard.tsx index 7ae82d28c4..1392b55420 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskCard.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskCard.tsx @@ -77,7 +77,6 @@ export const TaskCard = ({ }; const priorityColor = task.priority === 'HIGH' ? '#ef4345' : task.priority === 'LOW' ? '#00ab41' : '#FFA500'; - const isOverdue = task.deadline != null && new Date(task.deadline) < new Date() && task.status !== 'DONE'; return ( <> @@ -108,8 +107,7 @@ export const TaskCard = ({ sx={{ opacity: snapshot.isDragging ? 0.9 : 1, transform: snapshot.isDragging ? 'rotate(-2deg)' : '', - borderRadius: '5px', - ...(isOverdue && { border: '2px solid #ef4345' }) + borderRadius: '5px' }} elevation={snapshot.isDragging ? 3 : 1} > @@ -154,11 +152,8 @@ export const TaskCard = ({ )} {task.deadline && ( - - + + Due: {datePipe(task.deadline)} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskColumn.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskColumn.tsx index fe60129ad9..d0b4a9cba9 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskColumn.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskColumn.tsx @@ -1,6 +1,6 @@ import { Droppable } from '@hello-pangea/dnd'; import { Box, Typography, useTheme } from '@mui/material'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import { Project, Task, TaskStatus, TaskWithIndex } from 'shared'; import { statusNames, TaskCard } from '.'; import { NERButton } from '../../../../../components/NERButton'; @@ -13,38 +13,21 @@ export const TaskColumn = ({ status, tasks, project, - equalizedHeight, - isDragging, onEditTask, onDeleteTask, - onAddTask, - onHeightChange + onAddTask }: { - status: TaskStatus; + status: Task['status']; tasks: TaskWithIndex[]; project: Project; - equalizedHeight: number; - isDragging: boolean; onEditTask: (task: Task) => void; onDeleteTask: (taskId: string) => void; onAddTask: (task: Task) => void; - onHeightChange: (status: TaskStatus, height: number) => void; }) => { const { mutateAsync: createTask } = useCreateTask(); const [showCreateTaskModal, setShowCreateTaskModal] = useState(false); const toast = useToast(); const theme = useTheme(); - const droppableBoxRef = useRef(null); - - useEffect(() => { - const box = droppableBoxRef.current; - if (!box) return; - const observer = new ResizeObserver(() => { - onHeightChange(status, box.scrollHeight); - }); - observer.observe(box); - return () => observer.disconnect(); - }, [status, onHeightChange]); const handleCreateTask = async ({ notes, title, deadline, assignees, priority, startDate }: EditTaskFormInput) => { try { @@ -71,7 +54,6 @@ export const TaskColumn = ({ return ( <> setShowCreateTaskModal(false)} modalShow={showCreateTaskModal} @@ -80,37 +62,20 @@ export const TaskColumn = ({ {statusNames[status]} - setShowCreateTaskModal(true)} - > - + Add A Task - {(droppableProvided, snapshot) => ( { - droppableProvided.innerRef(droppableBox); - droppableBoxRef.current = droppableBox; - }} + ref={droppableProvided.innerRef} {...droppableProvided.droppableProps} className={snapshot.isDraggingOver ? ' isDraggingOver' : ''} sx={{ @@ -118,7 +83,6 @@ export const TaskColumn = ({ flexDirection: 'column', borderRadius: 5, padding: '5px', - flex: 1, '&.isDraggingOver': { bgcolor: '#dadadf' } @@ -138,6 +102,17 @@ export const TaskColumn = ({ )} + setShowCreateTaskModal(true)} + > + + Add A Task + ); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskList.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskList.tsx index 5f440961de..5077e09221 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskList.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskList.tsx @@ -1,10 +1,14 @@ -import { useMediaQuery, Theme } from '@mui/material'; +import { useMediaQuery, Typography, Theme } from '@mui/material'; import { Project } from 'shared'; import { TaskListContent } from './TaskListContent'; -import { GuestsTasksList } from '../GuestTasksList'; -export const TaskList = ({ project, isGuest }: { project: Project; isGuest: boolean }) => { +export const TaskList = ({ project }: { project: Project }) => { const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); - - return isSmall || isGuest ? : ; + return isSmall ? : ; }; + +const FallbackForMobile = () => ( + + The Kanban board is not available on mobile + +); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskListContent.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskListContent.tsx index 4ca6cbb121..fdb2b0b455 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskListContent.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskListContent.tsx @@ -1,7 +1,7 @@ -import { DragDropContext, OnDragEndResponder, OnDragStartResponder } from '@hello-pangea/dnd'; +import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; import { Box } from '@mui/material'; -import { useCallback, useState } from 'react'; -import { Project, Task, TaskStatus, TaskWithIndex } from 'shared'; +import { useState } from 'react'; +import { Project, Task, TaskWithIndex } from 'shared'; import { getTasksByStatus, statuses, TasksByStatus } from '.'; import { useSetTaskStatus } from '../../../../../hooks/tasks.hooks'; import { useToast } from '../../../../../hooks/toasts.hooks'; @@ -19,14 +19,6 @@ export const TaskListContent = ({ project }: TaskListProps) => { const toast = useToast(); - const [isDragging, setIsDragging] = useState(false); - const [columnHeights, setColumnHeights] = useState>>({}); - const equalizedHeight = Math.max(...(Object.values(columnHeights) as number[])); - - const onHeightChange = useCallback((status: TaskStatus, height: number) => { - setColumnHeights((prev) => ({ ...prev, [status]: height })); - }, []); - const onDeleteTask = (taskId: string) => { setTasksByStatus((prev) => { const newTasksByStatus = { ...prev }; @@ -62,12 +54,7 @@ export const TaskListContent = ({ project }: TaskListProps) => { })); }; - const onDragStart: OnDragStartResponder = () => { - setIsDragging(true); - }; - const onDragEnd: OnDragEndResponder = async (result) => { - setIsDragging(false); const { destination, source } = result; if (!destination) { @@ -123,20 +110,17 @@ export const TaskListContent = ({ project }: TaskListProps) => { }; return ( - + {statuses.map((status) => ( ))} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/statuses.ts b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/statuses.ts index cdc703babc..6a432ea068 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/statuses.ts +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/statuses.ts @@ -16,11 +16,8 @@ const rankTaskPriority = (priority: TaskPriority) => { return 0; }; -const compareTasks = (taskA: Task, taskB: Task) => { - const deadlineA = taskA.deadline ? new Date(taskA.deadline).getTime() : Infinity; - const deadlineB = taskB.deadline ? new Date(taskB.deadline).getTime() : Infinity; - if (deadlineA !== deadlineB) return deadlineA - deadlineB; - return rankTaskPriority(taskB.priority) - rankTaskPriority(taskA.priority); +const compareTaskPriorities = (priorityA: TaskPriority, priorityB: TaskPriority) => { + return rankTaskPriority(priorityA) - rankTaskPriority(priorityB); }; export const getTasksByStatus = (unorderedTasks: Task[]) => { @@ -31,9 +28,11 @@ export const getTasksByStatus = (unorderedTasks: Task[]) => { }, statuses.reduce((obj, status) => ({ ...obj, [status]: [] }), {} as TasksByStatus) ); - // order each column by due date, then priority as tiebreaker + // order each column by priority statuses.forEach((status) => { - postsByStatus[status] = postsByStatus[status].sort(compareTasks); + postsByStatus[status] = postsByStatus[status].sort((recordA: Task, recordB: Task) => + compareTaskPriorities(recordA.priority, recordB.priority) + ); }); return postsByStatus; }; diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 12ff2a0445..3758a0794a 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -5,11 +5,7 @@ import { useAllReimbursementRequests } from '../../hooks/finance.hooks'; import { useSingleProject } from '../../hooks/projects.hooks'; import { WbsNumber, ReimbursementRequest, WBSElementData, equalsWbsNumber, ReimbursementStatusType } from 'shared'; import LoadingIndicator from '../../components/LoadingIndicator'; -import { - createReimbursementRequestRowData, - cleanReimbursementRequestStatus, - getCurrentReimbursementStatus -} from '../../utils/reimbursement-request.utils'; +import { createReimbursementRequestRowData, cleanReimbursementRequestStatus } from '../../utils/reimbursement-request.utils'; import NERDataGrid, { MapRowResult } from '../../components/NERDataGrid'; import { routes } from '../../utils/routes'; import { fullNamePipe, centsToDollar, datePipe } from '../../utils/pipes'; @@ -70,10 +66,7 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const budgetInfo = useMemo(() => { if (!project) return null; const totalBudget = project.budget; // already in dollars - const nonDeniedRequests = reimbursementRequests.filter( - (rr) => getCurrentReimbursementStatus(rr.reimbursementStatuses).type !== 'DENIED' - ); - const totalSpent = nonDeniedRequests.reduce((sum, rr) => sum + getProjectCost(rr, wbsNum), 0) / 100; // cents → dollars + const totalSpent = reimbursementRequests.reduce((sum, rr) => sum + getProjectCost(rr, wbsNum), 0) / 100; // cents → dollars const budgetRemaining = totalBudget - totalSpent; const budgetUsedPercentage = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0; return { diff --git a/src/frontend/src/pages/ProjectsPage/ProjectsOverview.tsx b/src/frontend/src/pages/ProjectsPage/ProjectsOverview.tsx index 5c1c02b618..2539a7d3d8 100644 --- a/src/frontend/src/pages/ProjectsPage/ProjectsOverview.tsx +++ b/src/frontend/src/pages/ProjectsPage/ProjectsOverview.tsx @@ -64,17 +64,18 @@ const ProjectsOverview: React.FC = () => { favoriteProjectsSet={favoriteProjectsSet} emptyMessage="You have no favorite projects. Click the star on a project's page to add one!" /> - {filteredLeadingProjects.length > 0 && ( + + {filteredTeamsProjects.length > 0 && ( )} - {filteredTeamsProjects.length > 0 && ( + {filteredLeadingProjects.length > 0 && ( )} diff --git a/src/frontend/src/pages/ProjectsPage/ProjectsPage.tsx b/src/frontend/src/pages/ProjectsPage/ProjectsPage.tsx index 0d35d580dc..c50e69e35f 100644 --- a/src/frontend/src/pages/ProjectsPage/ProjectsPage.tsx +++ b/src/frontend/src/pages/ProjectsPage/ProjectsPage.tsx @@ -14,7 +14,6 @@ import { useCurrentUser } from '../../hooks/users.hooks'; import { isGuest } from 'shared'; import { Add } from '@mui/icons-material'; import { useHistory } from 'react-router-dom'; -import GuestProjectsPage from '../GuestProjectsPage/GuestProjectsPage'; /** * Cards of all projects that this user is in their team. @@ -25,9 +24,6 @@ const ProjectsPage: React.FC = () => { const user = useCurrentUser(); const history = useHistory(); - if (isGuest(user.role)) { - return ; - } return ( { const { isLoading, data, error } = useAllProjects(); - if (!localStorage.getItem('projectsTableRowCount')) localStorage.setItem('projectsTableRowCount', '30'); const [pageSize, setPageSize] = useState(localStorage.getItem('projectsTableRowCount')); const [windowSize, setWindowSize] = useState(window.innerWidth); diff --git a/src/frontend/src/pages/RetrospectivePage/Retrospective.tsx b/src/frontend/src/pages/RetrospectivePage/Retrospective.tsx index 40a9f4a78e..4584ed8375 100644 --- a/src/frontend/src/pages/RetrospectivePage/Retrospective.tsx +++ b/src/frontend/src/pages/RetrospectivePage/Retrospective.tsx @@ -152,6 +152,24 @@ const RetrospectivePage = () => { }; }); + const overdueHandler = [ + { + filterLabel: 'Overdue', + handler: (event: ChangeEvent) => + handleSetGanttFilters({ ...filters, showOnlyOverdue: event.target.checked }), + defaultChecked: filters.showOnlyOverdue + } + ]; + + const hideTasksHandler = [ + { + filterLabel: 'Hide Tasks', + handler: (event: ChangeEvent) => + handleSetGanttFilters({ ...filters, hideTasks: event.target.checked }), + defaultChecked: filters.hideTasks + } + ]; + const carHandlers: { filterLabel: string; handler: (event: ChangeEvent) => void; @@ -213,6 +231,8 @@ const RetrospectivePage = () => { carHandlers={carHandlers} teamTypeHandlers={teamTypeHandlers} teamHandlers={teamHandlers} + overdueHandler={overdueHandler} + hideTasksHandler={hideTasksHandler} resetHandler={resetHandler} /> diff --git a/src/frontend/src/pages/TeamsPage/TakeAttendanceModal.tsx b/src/frontend/src/pages/TeamsPage/TakeAttendanceModal.tsx deleted file mode 100644 index 41c6e7e36c..0000000000 --- a/src/frontend/src/pages/TeamsPage/TakeAttendanceModal.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { Controller, useForm } from 'react-hook-form'; -import { useToast } from '../../hooks/toasts.hooks'; -import * as yup from 'yup'; -import NERFormModal from '../../components/NERFormModal'; -import { Alert, Box, CircularProgress, FormControl, FormLabel, TextField, Typography } from '@mui/material'; -import { useCheckChannelName, useTakeAttendance } from '../../hooks/attendance.hooks'; - -interface TakeAttendanceInputs { - message: string; -} - -interface TakeAttendanceModalProps { - teamId: string; - teamName: string; - showModal: boolean; - onHide: () => void; -} - -const TakeAttendanceModal: React.FC = ({ teamId, teamName, showModal, onHide }) => { - const toast = useToast(); - const { mutateAsync: takeAttendance } = useTakeAttendance(); - const { data: channelData, isLoading: channelLoading } = useCheckChannelName(teamId, showModal); - - const schema = yup.object().shape({ - message: yup.string().required('Message is required') - }); - - const { - handleSubmit, - control, - reset, - formState: { isValid } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - message: `Please react to this message to confirm your attendance at today's ${teamName} meeting!` - } - }); - - const handleConfirm = async ({ message }: TakeAttendanceInputs) => { - try { - await takeAttendance({ teamId, message }); - onHide(); - toast.success('Attendance session started!'); - } catch (e) { - if (e instanceof Error) { - toast.error(e.message); - } - } - }; - - const channelValid = channelData?.valid; - const channelName = channelData?.channelName; - - return ( - - - {channelLoading ? ( - - - Checking Slack channel... - - ) : channelValid ? ( - - This will send a message in #{channelName} - - ) : ( - - The Slack ID for this team is invalid or the bot is not in the channel. Please update it in Admin Tools > - Miscellaneous and add the FinishLine bot to the channel. - - )} - - Message - ( - - )} - /> - - - - ); -}; - -export default TakeAttendanceModal; diff --git a/src/frontend/src/pages/TeamsPage/TeamSpecificPage.tsx b/src/frontend/src/pages/TeamsPage/TeamSpecificPage.tsx index 02f859dac0..dd1edefe07 100644 --- a/src/frontend/src/pages/TeamsPage/TeamSpecificPage.tsx +++ b/src/frontend/src/pages/TeamsPage/TeamSpecificPage.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, ListItemIcon, Menu, MenuItem, Stack, Typography } from '@mui/material'; +import { Box, Grid, ListItemIcon, Menu, MenuItem, Stack } from '@mui/material'; import { useArchiveTeam, useSingleTeam } from '../../hooks/teams.hooks'; import { useParams } from 'react-router-dom'; import TeamMembersPageBlock from './TeamMembersPageBlock'; @@ -11,13 +11,10 @@ import { routes } from '../../utils/routes'; import PageLayout from '../../components/PageLayout'; import { NERButton } from '../../components/NERButton'; import { useCurrentUser } from '../../hooks/users.hooks'; -import { useCloseAttendance, useOngoingAttendance } from '../../hooks/attendance.hooks'; import { isAdmin, isGuest, ReimbursementRequestData, WbsElementStatus } from 'shared'; import React, { useState } from 'react'; import DeleteTeamModal from './DeleteTeamModal'; import SetTeamTypeModal from './SetTeamTypeModal'; -import TakeAttendanceModal from './TakeAttendanceModal'; -import NERModal from '../../components/NERModal'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { TeamPill } from './TeamPill'; import { useToast } from '../../hooks/toasts.hooks'; @@ -41,11 +38,7 @@ const TeamSpecificPage: React.FC = () => { const user = useCurrentUser(); const [showDeleteModal, setDeleteModalShow] = useState(false); const [showTeamTypeModal, setShowTeamTypeModal] = useState(false); - const [showTakeAttendanceModal, setShowTakeAttendanceModal] = useState(false); - const [showCloseAttendanceConfirm, setShowCloseAttendanceConfirm] = useState(false); const [anchorEl, setAnchorEl] = useState(null); - const { data: ongoingAttendance } = useOngoingAttendance(teamId); - const { mutateAsync: closeAttendance } = useCloseAttendance(); const dropdownOpen = Boolean(anchorEl); const { mutateAsync: archiveTeam } = useArchiveTeam(teamId); const handleClickDelete = () => { @@ -92,31 +85,6 @@ const TeamSpecificPage: React.FC = () => { ); - const isAttendanceAuthorized = isAdmin(user.role) || user.userId === data.head.userId; - - const handleCloseAttendance = async () => { - try { - await closeAttendance(teamId); - setShowCloseAttendanceConfirm(false); - toast.success('Attendance closed successfully!'); - } catch (e: unknown) { - if (e instanceof Error) { - toast.error(e.message, 3000); - } - } - }; - - const AttendanceButton = () => - ongoingAttendance ? ( - setShowCloseAttendanceConfirm(true)} disabled={!isAttendanceAuthorized}> - Close Attendance - - ) : ( - setShowTakeAttendanceModal(true)} disabled={!isAttendanceAuthorized}> - Take Attendance - - ); - interface ArchiveTeamButtonProps { archive: boolean; } @@ -181,7 +149,6 @@ const TeamSpecificPage: React.FC = () => { - {TeamActionsDropdown} @@ -194,11 +161,7 @@ const TeamSpecificPage: React.FC = () => { ) : null } - previousPages={ - isGuest(user.role) && data.teamType - ? [{ name: data.teamType.name, route: `${routes.TEAMS}/${data.teamType.teamTypeId}` }] - : [{ name: 'Teams', route: routes.TEAMS }] - } + previousPages={[{ name: 'Teams', route: routes.TEAMS }]} > @@ -232,23 +195,6 @@ const TeamSpecificPage: React.FC = () => { setDeleteModalShow(false)} /> setShowTeamTypeModal(false)} /> - setShowTakeAttendanceModal(false)} - /> - setShowCloseAttendanceConfirm(false)} - title="Close Attendance" - submitText="Close Attendance" - onSubmit={handleCloseAttendance} - cancelText="Cancel" - > - Are you sure you want to close the ongoing attendance session for {data.teamName}? - This will delete the Slack message and end the attendance session. - ); }; diff --git a/src/frontend/src/pages/TeamsPage/Teams.tsx b/src/frontend/src/pages/TeamsPage/Teams.tsx index 7ba2318d05..30851923dc 100644 --- a/src/frontend/src/pages/TeamsPage/Teams.tsx +++ b/src/frontend/src/pages/TeamsPage/Teams.tsx @@ -7,32 +7,11 @@ import { Route, Switch } from 'react-router-dom'; import { routes } from '../../utils/routes'; import TeamsPage from './TeamsPage'; import TeamSpecificPage from './TeamSpecificPage'; -import { useParams } from 'react-router-dom'; -import { useCurrentUser } from '../../hooks/users.hooks'; -import { isGuest } from 'shared'; -import { useAllTeamTypes } from '../../hooks/team-types.hooks'; -import GuestTeamPage from '../GuestDivisionPage/GuestTeamPage'; -import LoadingIndicator from '../../components/LoadingIndicator'; -import ErrorPage from '../ErrorPage'; - -const TeamOrDivisionPage: React.FC = () => { - const { teamId } = useParams<{ teamId: string }>(); - const user = useCurrentUser(); - const { isLoading: teamsLoading, isError: isTeamsError, data: teamTypes, error: teamsError } = useAllTeamTypes(); - - if (isTeamsError) return ; - if (teamsLoading || !teamTypes) return ; - - if (isGuest(user.role) && teamTypes?.some((t) => t.teamTypeId === teamId)) { - return ; - } - return ; -}; const Teams: React.FC = () => { return ( - + ); diff --git a/src/frontend/src/pages/WorkPackageDetailPage/ActivateWorkPackageModalContainer/ActivateWorkPackageModalContainer.tsx b/src/frontend/src/pages/WorkPackageDetailPage/ActivateWorkPackageModalContainer/ActivateWorkPackageModalContainer.tsx index b76c7b6dfa..40c43ce61d 100644 --- a/src/frontend/src/pages/WorkPackageDetailPage/ActivateWorkPackageModalContainer/ActivateWorkPackageModalContainer.tsx +++ b/src/frontend/src/pages/WorkPackageDetailPage/ActivateWorkPackageModalContainer/ActivateWorkPackageModalContainer.tsx @@ -4,7 +4,7 @@ */ import { useHistory } from 'react-router-dom'; -import { ChangeRequestType, dateToMidnightUTC, WbsNumber, wbsPipe } from 'shared'; +import { ChangeRequestType, dateToMidnightUTC, WbsNumber } from 'shared'; import { useAuth } from '../../../hooks/auth.hooks'; import { useCreateActivationChangeRequest } from '../../../hooks/change-requests.hooks'; import { useAllMembers } from '../../../hooks/users.hooks'; @@ -57,7 +57,7 @@ const ActivateWorkPackageModalContainer: React.FC = ({ proposedSolutions: [] }); - history.push(`${routes.PROJECTS}/${wbsPipe(wbsElement.wbsNum)}/change-requests`); + history.push(routes.CHANGE_REQUESTS); } else { await workPackageMutateAsync(payload); exitActiveMode(); diff --git a/src/frontend/src/tests/app/AppContext.test.tsx b/src/frontend/src/tests/app/AppContext.test.tsx index 9232cfe0b8..f3ffdc7a2f 100644 --- a/src/frontend/src/tests/app/AppContext.test.tsx +++ b/src/frontend/src/tests/app/AppContext.test.tsx @@ -33,15 +33,6 @@ vi.mock('../../app/AppContextTheme', () => { }; }); -vi.mock('../../app/AppGlobalCarFilterContext', () => { - return { - __esModule: true, - GlobalCarFilterProvider: (props: { children: React.ReactNode }) => { - return
{props.children}
; - } - }; -}); - // Sets up the component under test with the desired values and renders it const renderComponent = () => { render( diff --git a/src/frontend/src/tests/app/AppContextQuery.test.tsx b/src/frontend/src/tests/app/AppContextQuery.test.tsx index 6c74de915f..415c35ebcb 100644 --- a/src/frontend/src/tests/app/AppContextQuery.test.tsx +++ b/src/frontend/src/tests/app/AppContextQuery.test.tsx @@ -7,16 +7,6 @@ import { render, screen } from '@testing-library/react'; // avoid circular depen import { useAllChangeRequests } from '../../hooks/change-requests.hooks'; import AppContextQuery from '../../app/AppContextQuery'; -vi.mock('../../app/AppGlobalCarFilterContext', () => ({ - useGlobalCarFilter: () => ({ - selectedCar: 'all-cars', - allCars: [], - setSelectedCar: vi.fn(), - isLoading: false, - error: null - }) -})); - describe('app context', () => { it('renders simple text as children', () => { render(hello); diff --git a/src/frontend/src/tests/hooks/ChangeRequests.hooks.test.tsx b/src/frontend/src/tests/hooks/ChangeRequests.hooks.test.tsx index 33762e979a..8ce2a1e18b 100644 --- a/src/frontend/src/tests/hooks/ChangeRequests.hooks.test.tsx +++ b/src/frontend/src/tests/hooks/ChangeRequests.hooks.test.tsx @@ -13,15 +13,6 @@ import { getAllChangeRequests, getSingleChangeRequest } from '../../apis/change- import { useAllChangeRequests, useSingleChangeRequest } from '../../hooks/change-requests.hooks'; vi.mock('../../apis/change-requests.api'); -vi.mock('../../app/AppGlobalCarFilterContext', () => ({ - useGlobalCarFilter: () => ({ - selectedCar: 'all-cars', - allCars: [], - setSelectedCar: vi.fn(), - isLoading: false, - error: null - }) -})); describe('change request hooks', () => { it('handles getting a list of change requests', async () => { @@ -29,7 +20,7 @@ describe('change request hooks', () => { mockedGetAllChangeRequests.mockReturnValue(mockPromiseAxiosResponse(exampleAllChangeRequests)); const { result } = renderHook(() => useAllChangeRequests(), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAllChangeRequests); }); @@ -38,7 +29,7 @@ describe('change request hooks', () => { mockedGetSingleChangeRequest.mockReturnValue(mockPromiseAxiosResponse(exampleStageGateChangeRequest)); const { result } = renderHook(() => useSingleChangeRequest('1'), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleStageGateChangeRequest); }); }); diff --git a/src/frontend/src/tests/hooks/GlobalCarFilterContext.test.tsx b/src/frontend/src/tests/hooks/GlobalCarFilterContext.test.tsx deleted file mode 100644 index 5162f79b99..0000000000 --- a/src/frontend/src/tests/hooks/GlobalCarFilterContext.test.tsx +++ /dev/null @@ -1,233 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { renderHook, render, screen, act, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from 'react-query'; -import { GlobalCarFilterProvider, useGlobalCarFilter } from '../../app/AppGlobalCarFilterContext'; -import * as carsHooks from '../../hooks/cars.hooks'; -import { exampleAllCars } from '../test-support/test-data/cars.stub'; - -// Mock the hooks -vi.mock('../../hooks/cars.hooks'); -const mockUseGetAllCars = vi.mocked(carsHooks.useGetAllCars); - -// Create wrapper with providers -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false } - } - }); - - return ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); -}; - -describe('useGlobalCarFilter', () => { - beforeEach(() => { - localStorage.clear(); - vi.clearAllMocks(); - }); - - it('should default to the most recent car when no saved car id in local storage', async () => { - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - const mostRecentCar = exampleAllCars.reduce((a, b) => (a.wbsNum.carNumber > b.wbsNum.carNumber ? a : b)); - expect(result.current.selectedCar).toEqual(mostRecentCar); - expect(localStorage.getItem('selectedCarId')).toBe(mostRecentCar.id); - }); - - it('should restore car from local storage by id', async () => { - localStorage.setItem('selectedCarId', exampleAllCars[0].id); - - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.selectedCar).toEqual(exampleAllCars[0]); - }); - }); - - it('should restore "all-cars" from local storage', async () => { - localStorage.setItem('selectedCarId', 'all-cars'); - - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.selectedCar).toBe('all-cars'); - }); - - it('should default to most recent car when saved car id does not match any car', async () => { - localStorage.setItem('selectedCarId', 'nonexistent-id'); - - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - const mostRecentCar = exampleAllCars.reduce((a, b) => (a.wbsNum.carNumber > b.wbsNum.carNumber ? a : b)); - expect(result.current.selectedCar).toEqual(mostRecentCar); - }); - - it('should persist car id to local storage when selecting a car', async () => { - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - act(() => { - result.current.setSelectedCar(exampleAllCars[1]); - }); - - expect(localStorage.getItem('selectedCarId')).toBe(exampleAllCars[1].id); - expect(result.current.selectedCar).toEqual(exampleAllCars[1]); - }); - - it('should store "all-cars" in local storage when selecting all cars', async () => { - localStorage.setItem('selectedCarId', exampleAllCars[0].id); - - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.selectedCar).toEqual(exampleAllCars[0]); - }); - - act(() => { - result.current.setSelectedCar('all-cars'); - }); - - expect(localStorage.getItem('selectedCarId')).toBe('all-cars'); - expect(result.current.selectedCar).toBe('all-cars'); - }); - - it('should render a loading indicator while cars are being fetched', () => { - mockUseGetAllCars.mockReturnValue({ - data: undefined, - isLoading: true, - error: null - } as any); - - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false }, mutations: { retry: false } } - }); - - render( - - -
- - - ); - - expect(screen.getByTestId('loader')).toBeInTheDocument(); - expect(screen.queryByTestId('children')).toBeNull(); - }); - - it('should handle error state', () => { - const error = new Error('Failed to load cars'); - - mockUseGetAllCars.mockReturnValue({ - data: undefined, - isLoading: false, - error - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - expect(result.current.error).toBe(error); - expect(result.current.isLoading).toBe(false); - }); - - it('should update local storage when switching between cars', async () => { - mockUseGetAllCars.mockReturnValue({ - data: exampleAllCars, - isLoading: false, - error: null - } as any); - - const { result } = renderHook(() => useGlobalCarFilter(), { - wrapper: createWrapper() - }); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - act(() => { - result.current.setSelectedCar(exampleAllCars[0]); - }); - - expect(localStorage.getItem('selectedCarId')).toBe(exampleAllCars[0].id); - - act(() => { - result.current.setSelectedCar(exampleAllCars[2]); - }); - - expect(localStorage.getItem('selectedCarId')).toBe(exampleAllCars[2].id); - expect(result.current.selectedCar).toEqual(exampleAllCars[2]); - }); -}); diff --git a/src/frontend/src/tests/hooks/Projects.hooks.test.tsx b/src/frontend/src/tests/hooks/Projects.hooks.test.tsx index 04485ff008..c35e2b989c 100644 --- a/src/frontend/src/tests/hooks/Projects.hooks.test.tsx +++ b/src/frontend/src/tests/hooks/Projects.hooks.test.tsx @@ -6,15 +6,13 @@ import { renderHook, waitFor } from '@testing-library/react'; import { AxiosResponse } from 'axios'; import { Project } from 'shared'; -import AppContextQuery from '../../app/AppContextQuery'; +import wrapper from '../../app/AppContextQuery'; import { mockPromiseAxiosResponse } from '../test-support/test-data/test-utils.stub'; import { exampleAllProjects, exampleProject1 } from '../test-support/test-data/projects.stub'; import { exampleWbsProject1 } from '../test-support/test-data/wbs-numbers.stub'; import { getAllProjectsGantt, getSingleProject } from '../../apis/projects.api'; import { useAllProjectsGantt, useSingleProject } from '../../hooks/projects.hooks'; -const wrapper = ({ children }: { children: React.ReactNode }) => {children}; - vi.mock('../../apis/projects.api'); describe('project hooks', () => { @@ -23,7 +21,7 @@ describe('project hooks', () => { mockedGetAllProjects.mockReturnValue(mockPromiseAxiosResponse(exampleAllProjects)); const { result } = renderHook(() => useAllProjectsGantt(), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAllProjects); }); @@ -32,7 +30,7 @@ describe('project hooks', () => { mockedGetSingleProject.mockReturnValue(mockPromiseAxiosResponse(exampleProject1)); const { result } = renderHook(() => useSingleProject(exampleWbsProject1), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleProject1); }); }); diff --git a/src/frontend/src/tests/hooks/Users.hooks.test.tsx b/src/frontend/src/tests/hooks/Users.hooks.test.tsx index 098064aab8..400a2bc11e 100644 --- a/src/frontend/src/tests/hooks/Users.hooks.test.tsx +++ b/src/frontend/src/tests/hooks/Users.hooks.test.tsx @@ -22,7 +22,7 @@ describe('user hooks', () => { mockedGetAllOrgUsers.mockReturnValue(mockPromiseAxiosResponse(exampleAllUsers)); const { result } = renderHook(() => useAllUsers(), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAllUsers); }); @@ -31,7 +31,7 @@ describe('user hooks', () => { mockedGetSingleUser.mockReturnValue(mockPromiseAxiosResponse(exampleAdminUser)); const { result } = renderHook(() => useSingleUser('1'), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAdminUser); }); @@ -47,7 +47,7 @@ describe('user hooks', () => { result.current.mutate(exampleAdminUser.email); }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAdminUser); }); }); diff --git a/src/frontend/src/tests/hooks/WorkPackages.hooks.test.tsx b/src/frontend/src/tests/hooks/WorkPackages.hooks.test.tsx index 54cc5b466a..3b4ce0a8d5 100644 --- a/src/frontend/src/tests/hooks/WorkPackages.hooks.test.tsx +++ b/src/frontend/src/tests/hooks/WorkPackages.hooks.test.tsx @@ -6,28 +6,14 @@ import { renderHook, waitFor } from '@testing-library/react'; import { AxiosResponse } from 'axios'; import { WorkPackage } from 'shared'; -import AppContextQuery from '../../app/AppContextQuery'; -import { GlobalCarFilterProvider } from '../../app/AppGlobalCarFilterContext'; +import wrapper from '../../app/AppContextQuery'; import { mockPromiseAxiosResponse } from '../test-support/test-data/test-utils.stub'; import { exampleAllWorkPackages, exampleResearchWorkPackage } from '../test-support/test-data/work-packages.stub'; import { exampleWbsWorkPackage1 } from '../test-support/test-data/wbs-numbers.stub'; import { getAllWorkPackages, getSingleWorkPackage } from '../../apis/work-packages.api'; import { useAllWorkPackages, useSingleWorkPackage } from '../../hooks/work-packages.hooks'; -import * as carsHooks from '../../hooks/cars.hooks'; -import { exampleAllCars } from '../test-support/test-data/cars.stub'; - -const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); vi.mock('../../apis/work-packages.api'); -vi.mock('../../hooks/cars.hooks'); - -beforeEach(() => { - vi.mocked(carsHooks.useGetAllCars).mockReturnValue({ data: exampleAllCars, isLoading: false, error: null } as any); -}); describe('work package hooks', () => { it('handles getting a list of work packages', async () => { @@ -35,7 +21,7 @@ describe('work package hooks', () => { mockedGetAllWorkPackages.mockReturnValue(mockPromiseAxiosResponse(exampleAllWorkPackages)); const { result } = renderHook(() => useAllWorkPackages(), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleAllWorkPackages); }); @@ -46,7 +32,7 @@ describe('work package hooks', () => { const { result } = renderHook(() => useSingleWorkPackage(exampleWbsWorkPackage1), { wrapper }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => result.current.isSuccess); expect(result.current.data).toEqual(exampleResearchWorkPackage); }); }); diff --git a/src/frontend/src/tests/layouts/Sidebar/Sidebar.test.tsx b/src/frontend/src/tests/layouts/Sidebar/Sidebar.test.tsx index ddeb70d78c..fb1231df40 100644 --- a/src/frontend/src/tests/layouts/Sidebar/Sidebar.test.tsx +++ b/src/frontend/src/tests/layouts/Sidebar/Sidebar.test.tsx @@ -11,16 +11,6 @@ import Sidebar from '../../../layouts/Sidebar/Sidebar'; import { ToastContext, ToastInputs } from '../../../components/Toast/ToastProvider'; import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/authenticated-user.stub'; -vi.mock('../../../app/AppGlobalCarFilterContext', () => ({ - useGlobalCarFilter: () => ({ - selectedCar: 'all-cars', - allCars: [], - setSelectedCar: vi.fn(), - isLoading: false, - error: null - }) -})); - const addToast = (message: ToastInputs) => { console.log(message); }; diff --git a/src/frontend/src/tests/pages/ChangeRequestDetailPage/ChangeRequestDetailsView.test.tsx b/src/frontend/src/tests/pages/ChangeRequestDetailPage/ChangeRequestDetailsView.test.tsx index 3547c9babc..292bfed62e 100644 --- a/src/frontend/src/tests/pages/ChangeRequestDetailPage/ChangeRequestDetailsView.test.tsx +++ b/src/frontend/src/tests/pages/ChangeRequestDetailPage/ChangeRequestDetailsView.test.tsx @@ -24,15 +24,6 @@ import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/auth vi.mock('../../../hooks/projects.hooks'); vi.mock('../../../hooks/users.hooks'); -vi.mock('../../../app/AppGlobalCarFilterContext', () => ({ - useGlobalCarFilter: () => ({ - selectedCar: 'all-cars', - allCars: [], - setSelectedCar: vi.fn(), - isLoading: false, - error: null - }) -})); const mockedUseSingleProject = useSingleProject as jest.Mock>; const mockSingleProjectHook = (isLoading: boolean, isError: boolean, data?: Project, error?: Error) => { diff --git a/src/frontend/src/tests/pages/HomePage/Home.test.tsx b/src/frontend/src/tests/pages/HomePage/Home.test.tsx index 940067d840..e337d2d16e 100644 --- a/src/frontend/src/tests/pages/HomePage/Home.test.tsx +++ b/src/frontend/src/tests/pages/HomePage/Home.test.tsx @@ -13,16 +13,6 @@ import { mockAuth } from '../../test-support/test-data/test-utils.stub'; import { mockUseSingleUserSettings } from '../../test-support/mock-hooks'; import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/authenticated-user.stub'; -vi.mock('../../../app/AppGlobalCarFilterContext', () => ({ - useGlobalCarFilter: () => ({ - selectedCar: 'all-cars', - allCars: [], - setSelectedCar: vi.fn(), - isLoading: false, - error: null - }) -})); - vi.mock('../../../pages/HomePage/components/UsefulLinks', () => { return { __esModule: true, diff --git a/src/frontend/src/tests/pages/WorkPackageDetailPage/StageGateWorkPackageModalContainer/StageGateWorkPackageModal.test.tsx b/src/frontend/src/tests/pages/WorkPackageDetailPage/StageGateWorkPackageModalContainer/StageGateWorkPackageModal.test.tsx index e8d4cbd5c9..2b415b2208 100644 --- a/src/frontend/src/tests/pages/WorkPackageDetailPage/StageGateWorkPackageModalContainer/StageGateWorkPackageModal.test.tsx +++ b/src/frontend/src/tests/pages/WorkPackageDetailPage/StageGateWorkPackageModalContainer/StageGateWorkPackageModal.test.tsx @@ -7,7 +7,6 @@ import { render, screen, routerWrapperBuilder } from '../../../test-support/test import { wbsPipe } from '../../../../utils/pipes'; import { exampleWbs1 } from '../../../test-support/test-data/wbs-numbers.stub'; import StageGateWorkPackageModal from '../../../../pages/WorkPackageDetailPage/StageGateWorkPackageModalContainer/StageGateWorkPackageModal'; -import { ToastProvider } from '../../../../components/Toast/ToastProvider'; /** * Mock function for submitting the form, use if there is additional functionality added while submitting @@ -25,14 +24,12 @@ const renderComponent = (modalShow: boolean) => { const RouterWrapper = routerWrapperBuilder({}); return render( - - - + ); }; diff --git a/src/frontend/src/tests/test-support/test-data/cars.stub.ts b/src/frontend/src/tests/test-support/test-data/cars.stub.ts deleted file mode 100644 index db7d813004..0000000000 --- a/src/frontend/src/tests/test-support/test-data/cars.stub.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of NER's FinishLine and licensed under GNU AGPLv3. - * See the LICENSE file in the repository root folder for details. - */ - -import { Car, WbsElementStatus } from 'shared'; - -export const exampleCar1: Car = { - wbsElementId: 'wbs-element-1', - id: 'car-1', - name: 'Car 2023', - wbsNum: { - carNumber: 23, - projectNumber: 0, - workPackageNumber: 0 - }, - dateCreated: new Date('2023-01-01'), - deleted: false, - status: WbsElementStatus.Active, - links: [], - changes: [], - descriptionBullets: [] -}; - -export const exampleCar2: Car = { - wbsElementId: 'wbs-element-2', - id: 'car-2', - name: 'Car 2024', - wbsNum: { - carNumber: 24, - projectNumber: 0, - workPackageNumber: 0 - }, - dateCreated: new Date('2024-01-01'), - deleted: false, - status: WbsElementStatus.Active, - links: [], - changes: [], - descriptionBullets: [] -}; - -export const exampleCar3: Car = { - wbsElementId: 'wbs-element-3', - id: 'car-3', - name: 'Car 2025', - wbsNum: { - carNumber: 25, - projectNumber: 0, - workPackageNumber: 0 - }, - dateCreated: new Date('2025-01-01'), - deleted: false, - status: WbsElementStatus.Active, - links: [], - changes: [], - descriptionBullets: [] -}; - -export const exampleAllCars: Car[] = [exampleCar1, exampleCar2, exampleCar3]; - -export const exampleCurrentCar: Car = exampleCar3; // Latest car by car number - -// Additional test data for global car filter -export const exampleEmptyCarArray: Car[] = []; - -export const exampleSingleCar: Car[] = [exampleCar3]; diff --git a/src/frontend/src/utils/axios.ts b/src/frontend/src/utils/axios.ts index 410d6843b7..85eb74d43a 100644 --- a/src/frontend/src/utils/axios.ts +++ b/src/frontend/src/utils/axios.ts @@ -1,23 +1,9 @@ import axiosStatic from 'axios'; -declare module 'axios' { - interface AxiosRequestConfig { - overrideCarId?: string | 'all-cars'; - } -} - const axios = axiosStatic.create({ withCredentials: import.meta.env.MODE !== 'development' ? true : undefined }); -// holds the validated car UUID in memory, set by GlobalCarFilterProvider after login. -// Storing only in memory prevents stale UUIDs from being sent -// before the car list has been loaded and validated post-login. -let currentCarId: string | null = null; -export const setCurrentCarId = (id: string | null) => { - currentCarId = id; -}; - // This allows us to get good server errors // All express responses must be: res.status(404).json({ message: "You are not authorized to do that." }) axios.interceptors.response.use( @@ -51,11 +37,6 @@ axios.interceptors.request.use( if (import.meta.env.MODE === 'development') request.headers!['Authorization'] = localStorage.getItem('devUserId') || ''; const organizationId = localStorage.getItem('organizationId'); request.headers!['organizationId'] = organizationId ?? ''; - if (request.overrideCarId !== undefined) { - if (request.overrideCarId !== 'all-cars') request.headers!['carId'] = request.overrideCarId; - } else if (currentCarId) { - request.headers!['carId'] = currentCarId; - } return request; }, (error) => { diff --git a/src/frontend/src/utils/bom.utils.ts b/src/frontend/src/utils/bom.utils.ts index 9b78ff43a8..79aa5950e6 100644 --- a/src/frontend/src/utils/bom.utils.ts +++ b/src/frontend/src/utils/bom.utils.ts @@ -20,7 +20,6 @@ export interface BomRow extends GridValidRowModel { link: string; notes: string | undefined; assemblyId: string | undefined; - isCopied: boolean; } export const materialToRow = (material: Material, idx: number): BomRow => { @@ -39,8 +38,7 @@ export const materialToRow = (material: Material, idx: number): BomRow => { subtotal: material.subtotal !== undefined ? `$${centsToDollar(material.subtotal)}` : '', link: material.linkUrl, notes: material.notes, - assemblyId: material.assemblyId ?? 'assembly-misc', - isCopied: material.isCopied + assemblyId: material.assemblyId ?? 'assembly-misc' }; }; diff --git a/src/frontend/src/utils/routes.ts b/src/frontend/src/utils/routes.ts index fece77773b..5194281381 100644 --- a/src/frontend/src/utils/routes.ts +++ b/src/frontend/src/utils/routes.ts @@ -66,7 +66,6 @@ const PROJECT_TEMPLATE_EDIT = PROJECT_TEMPLATES + '/edit'; /**************** Design Review Calendar ****************/ const CALENDAR = `/calendar`; -const EVENTS = '/events'; /**************** Organizations ****************/ const ORGANIZATIONS = `/organizations`; @@ -136,7 +135,6 @@ export const routes = { PROJECT_TEMPLATE_EDIT, CALENDAR, - EVENTS, ORGANIZATIONS, diff --git a/src/frontend/src/utils/teams.utils.ts b/src/frontend/src/utils/teams.utils.ts index 6487ca9124..2c31114395 100644 --- a/src/frontend/src/utils/teams.utils.ts +++ b/src/frontend/src/utils/teams.utils.ts @@ -46,10 +46,6 @@ export type SubmitText = | 'Create Change Request' | 'Update' | 'Submit Vendor' - | 'Copy' - | 'Accept' - | 'Send' - | 'Close Attendance' - | 'Copy BOM'; + | 'Accept'; export type CancelText = 'Cancel' | 'Delete' | 'Exit' | 'No'; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 4ccc856fc2..5aca0c86e3 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -120,7 +120,6 @@ const homePageWorkPackages = (selection: WorkPackageSelection) => `${workPackage /**************** Change Requests Endpoints ****************/ const changeRequests = () => `${API_URL}/change-requests`; -const guestChangeRequests = () => `${API_URL}/change-requests/guest`; const toReviewChangeRequests = () => `${API_URL}/change-requests/to-review`; const unreviewedChangeRequests = (wbsNum?: WbsNumber) => `${API_URL}/change-requests/unreviewed` + (wbsNum ? `?wbsnum=${wbsPipe(wbsNum)}` : ''); @@ -226,72 +225,90 @@ const financeEditOtherReimbursementProductReason = (id: String) => const getReimbursementRequestProjectData = (projectId: string, startDate?: Date, endDate?: Date): string => { const url = new URL(`${financeRoutesEndpoints()}/reimbursement-request-project-data/${projectId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getReimbursementRequestTeamData = (teamId: string, startDate?: Date, endDate?: Date): string => { +const getReimbursementRequestTeamData = (teamId: string, startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/reimbursement-request-team-data/${teamId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getReimbursementRequestCategoryData = (otherReasonId: string, startDate?: Date, endDate?: Date): string => { +const getReimbursementRequestCategoryData = ( + otherReasonId: string, + startDate?: Date, + endDate?: Date, + carNumber?: number +): string => { const url = new URL(`${financeRoutesEndpoints()}/reimbursement-request-category-data/${otherReasonId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getAllReimbursementRequestData = (startDate?: Date, endDate?: Date): string => { +const getAllReimbursementRequestData = (startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/reimbursement-request-data`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getReimbursementRequestTeamTypeData = (teamTypeId: string, startDate?: Date, endDate?: Date): string => { +const getReimbursementRequestTeamTypeData = ( + teamTypeId: string, + startDate?: Date, + endDate?: Date, + carNumber?: number +): string => { const url = new URL(`${financeRoutesEndpoints()}/reimbursement-request-team-type-data/${teamTypeId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getSpendingBarTeamData = (teamId: string, startDate?: Date, endDate?: Date): string => { +const getSpendingBarTeamData = (teamId: string, startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/spending-bar-team-data/${teamId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getSpendingBarTeamTypeData = (teamTypeId: string, startDate?: Date, endDate?: Date): string => { +const getSpendingBarTeamTypeData = (teamTypeId: string, startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/spending-bar-team-type-data/${teamTypeId}`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getSpendingBarCategoryData = (startDate?: Date, endDate?: Date): string => { +const getSpendingBarCategoryData = (startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/spending-bar-category-data`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; -const getAllSpendingBarData = (startDate?: Date, endDate?: Date): string => { +const getAllSpendingBarData = (startDate?: Date, endDate?: Date, carNumber?: number): string => { const url = new URL(`${financeRoutesEndpoints()}/spending-bar-data`); const params = new URLSearchParams(); - if (startDate) params.set('startDate', new Date(startDate).toISOString()); - if (endDate) params.set('endDate', new Date(endDate).toISOString()); + if (startDate) params.set('startDate', startDate.toISOString()); + if (endDate) params.set('endDate', endDate.toISOString()); + if (carNumber !== undefined) params.set('carNumber', carNumber.toString()); const queryString = params.toString(); return queryString ? `${url.toString()}?${queryString}` : url.toString(); }; @@ -436,15 +453,14 @@ const deleteGraphCollection = (id: string) => `${graphCollectionById(id)}/delete /************** Retrospective Endpoints ***************/ const retrospectiveTimelines = (startDate?: Date, endDate?: Date) => `${API_URL}/retrospective/timelines?` + - (startDate ? `start=${encodeURIComponent(new Date(startDate).toISOString())}` : '') + - (endDate ? `end=${encodeURIComponent(new Date(endDate).toISOString())}` : ''); + (startDate ? `start=${encodeURIComponent(startDate.toISOString())}` : '') + + (endDate ? `end=${encodeURIComponent(endDate.toISOString())}` : ''); const retrospectiveBudgets = () => `${API_URL}/retrospective/budgets`; /**************** Calendar Endpoints ****************/ const calendar = () => `${API_URL}/calendar`; const calendarShops = () => `${calendar()}/shops`; const calendarEvents = () => `${calendar()}/events`; -const calendarEventsPaginated = () => `${calendar()}/events-paginated`; const calendarEventTypes = () => `${calendar()}/event-types`; const calendarCreateShop = () => `${calendar()}/shop/create`; const calendarFilterEvents = () => `${calendar()}/events/filter`; @@ -482,15 +498,6 @@ const calendarUploadDocument = (eventId: string) => `${calendar()}/event/${event const calendarPDFById = (fileId: string) => `${calendar()}/document/${fileId}`; const calendarScheduleEvent = (eventId: string) => `${calendar()}/event/${eventId}/schedule`; -/**************** Attendance Endpoints ****************/ -const attendance = () => `${API_URL}/attendance`; -const attendanceTakeAttendance = () => `${attendance()}/`; -const attendanceGetAll = () => `${attendance()}/`; -const attendanceCheckChannel = (teamId: string) => `${attendance()}/check-channel/${teamId}`; -const attendanceGetOngoing = (teamId: string) => `${attendance()}/ongoing/${teamId}`; -const attendanceCloseOngoing = (teamId: string) => `${attendance()}/close/${teamId}`; -const attendanceGetById = (meetingAttendanceId: string) => `${attendance()}/${meetingAttendanceId}`; - /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -589,7 +596,6 @@ export const apiUrls = { homePageWorkPackages, changeRequests, - guestChangeRequests, changeRequestsById, changeRequestsReview, changeRequestDelete, @@ -829,7 +835,6 @@ export const apiUrls = { calendarGetSingleEventWithMembers, calendarGetConflictingEvent, calendarEvents, - calendarEventsPaginated, calendarEventTypes, calendarDeleteEvent, calendarEventSetStatus, @@ -852,12 +857,5 @@ export const apiUrls = { calendarDeleteScheduleSlot, calendarScheduleEvent, - attendanceTakeAttendance, - attendanceGetAll, - attendanceCheckChannel, - attendanceGetOngoing, - attendanceCloseOngoing, - attendanceGetById, - version }; diff --git a/src/shared/index.ts b/src/shared/index.ts index 123ca845f7..475fee3d35 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -11,14 +11,13 @@ export * from './src/types/team-types.js'; export * from './src/types/task-types.js'; export * from './src/types/finance-types.js'; export * from './src/types/reimbursement-requests-types.js'; -export * from './src/types/recruitment-types.js'; +export * from './src/types/frequently-asked-questions-types.js'; export * from './src/types/milestone-types.js'; export * from './src/types/checklist-types.js'; export * from './src/types/pop-up-types.js'; export * from './src/types/announcements.types.js'; export * from './src/types/part-review.types.js'; export * from './src/types/calendar-types.js'; -export * from './src/types/attendance-types.js'; export * from './src/validate-wbs.js'; export * from './src/date-utils.js'; diff --git a/src/shared/package.json b/src/shared/package.json index d2200e32b2..06aaf7e964 100644 --- a/src/shared/package.json +++ b/src/shared/package.json @@ -23,7 +23,7 @@ "author": "", "license": "ISC", "devDependencies": { - "@types/node": "^25.0.0", + "@types/node": "18.17.1", "ts-node": "^8.10.1", "typescript": "^5.7.3" }, diff --git a/src/shared/src/types/attendance-types.ts b/src/shared/src/types/attendance-types.ts deleted file mode 100644 index 36a63033ee..0000000000 --- a/src/shared/src/types/attendance-types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { User } from './user-types.js'; - -export interface MeetingAttendance { - meetingAttendanceId: string; - teamId: string; - teamName: string; - userCreated: User; - openedAt: Date; - closedAt?: Date; - attendeesCount: number; - teamMemberAttendancePercent: number; -} - -export interface MeetingAttendanceWithAttendees extends MeetingAttendance { - attendees: User[]; -} diff --git a/src/shared/src/types/bom-types.ts b/src/shared/src/types/bom-types.ts index 4d0ca39ffa..c2d052c67e 100644 --- a/src/shared/src/types/bom-types.ts +++ b/src/shared/src/types/bom-types.ts @@ -77,7 +77,6 @@ export interface Material { linkUrl: string; notes?: string; reimbursementRequests: MaterialReimbursementRequest[]; - isCopied: boolean; } export type MaterialPreview = Omit< diff --git a/src/shared/src/types/change-request-types.ts b/src/shared/src/types/change-request-types.ts index 1b1f2b9060..a57b56b354 100644 --- a/src/shared/src/types/change-request-types.ts +++ b/src/shared/src/types/change-request-types.ts @@ -64,19 +64,6 @@ export interface ProposedSolution { approved: boolean; } -export interface GuestChangeRequest { - crId: string; - submitter: User; - identifier: number; - type: ChangeRequestType; - status: ChangeRequestStatus; - teamTypeNames: string[]; - accepted?: boolean; - reviewer?: User; - wbsNum?: WbsNumber; - wbsName?: string; -} - export interface ActivationChangeRequest extends ChangeRequest { lead: User; manager: User; diff --git a/src/shared/src/types/recruitment-types.ts b/src/shared/src/types/frequently-asked-questions-types.ts similarity index 67% rename from src/shared/src/types/recruitment-types.ts rename to src/shared/src/types/frequently-asked-questions-types.ts index c018591716..fd9b32eacb 100644 --- a/src/shared/src/types/recruitment-types.ts +++ b/src/shared/src/types/frequently-asked-questions-types.ts @@ -14,13 +14,3 @@ export interface FrequentlyAskedQuestion { dateCreated: Date; dateDeleted?: Date; } - -export interface GuestDefinition { - definitionId: string; - term: string; - description: string; - order: number; - buttonText?: string; - buttonLink?: string; - icon?: string; -} diff --git a/src/shared/src/types/project-types.ts b/src/shared/src/types/project-types.ts index 97316b6f9d..3b43e1129c 100644 --- a/src/shared/src/types/project-types.ts +++ b/src/shared/src/types/project-types.ts @@ -82,12 +82,11 @@ export interface ProjectPreview extends WbsElementPreview { abbreviation?: string; workPackages: WorkPackagePreview[]; teams: { teamName: string; teamId: string }[]; - teamTypes: { name: string; teamTypeId: string }[]; } export interface ProjectOverview extends ProjectPreview { links: Link[]; - tasksRemaining: number; + tasks: Task[]; } export interface RetrospectiveWorkPackage extends WorkPackage { diff --git a/system-tests/cypress.config.js b/system-tests/cypress.config.js index d349927eaf..cbe2689ab4 100644 --- a/system-tests/cypress.config.js +++ b/system-tests/cypress.config.js @@ -11,7 +11,6 @@ module.exports = { defaultCommandTimeout: 10000 }, env: { - base_url: 'http://localhost:3000', - backend_url: 'http://localhost:3001' + base_url: 'http://localhost:3000' } }; diff --git a/system-tests/cypress/e2e/home/home-page.cy.js b/system-tests/cypress/e2e/home/home-page.cy.js index 26c519e45b..852b403345 100644 --- a/system-tests/cypress/e2e/home/home-page.cy.js +++ b/system-tests/cypress/e2e/home/home-page.cy.js @@ -23,7 +23,6 @@ describe('Home Page', () => { }); it('Overdue Work Packages Contains At Least One Entry', () => { - cy.contains('Impact Attenuator').scrollIntoView(); cy.contains('Impact Attenuator').should(VISIBLE); }); diff --git a/system-tests/cypress/e2e/projects/projects-overview.cy.js b/system-tests/cypress/e2e/projects/projects-overview.cy.js index 37222ee1c1..53188cddfc 100644 --- a/system-tests/cypress/e2e/projects/projects-overview.cy.js +++ b/system-tests/cypress/e2e/projects/projects-overview.cy.js @@ -36,7 +36,7 @@ describe('Projects Overview', () => { // Fill in Project Name cy.get('[placeholder="Enter project name..."]').type(projectName); - // Car is pre-selected (NER-25), keep default + // Car is pre-selected (Miles), keep default // Select a Team // Target the Teams label (not the sidebar link) and find its sibling combobox diff --git a/system-tests/cypress/support/commands.js b/system-tests/cypress/support/commands.js index b20407b964..f419378d93 100644 --- a/system-tests/cypress/support/commands.js +++ b/system-tests/cypress/support/commands.js @@ -18,22 +18,6 @@ Cypress.Commands.add('login', (username = 'Thomas Emrax', redirect = '/home') => cy.contains(username).click(); cy.get(LOGIN_ICON).click(); cy.waitForLoading(); - // Login is complete, devUserId and organizationId are now in localStorage. - // Make an authenticated request directly to the API to resolve NER-25's car ID - // (UUID changes each seed), then persist it before the redirect so - // GlobalCarFilterProvider restores it on first mount. - cy.window().then((win) => { - const devUserId = win.localStorage.getItem('devUserId'); - const organizationId = win.localStorage.getItem('organizationId'); - cy.request({ - method: 'GET', - url: `${Cypress.env('backend_url')}/cars`, - headers: { Authorization: devUserId || '', organizationId: organizationId || '' } - }).then(({ body }) => { - const ner25 = body.find((car) => car.name === 'NER-25'); - if (ner25) win.localStorage.setItem('selectedCarId', ner25.id); - }); - }); cy.visit(Cypress.env('base_url') + redirect); cy.waitForLoading(); }); diff --git a/system-tests/cypress/utils/change-request.utils.cy.js b/system-tests/cypress/utils/change-request.utils.cy.js index 9653350a8a..27f26385d7 100644 --- a/system-tests/cypress/utils/change-request.utils.cy.js +++ b/system-tests/cypress/utils/change-request.utils.cy.js @@ -32,7 +32,7 @@ const createProposedSolution = ({ }; export const createChangeRequest = ({ - wbsTitle = '25.1.0 - Impact Attenuator', + wbsTitle = '0.1.0 - Impact Attenuator', what = 'test what', type = 'ISSUE', whys = [ @@ -76,6 +76,6 @@ export const createChangeRequest = ({ cy.contains(SUBMIT_BUTTON).click(); cy.url().should(INCLUDE, '/change-requests'); - // Verify the created CR appears in Un-reviewed Change Requests - cy.get(CR_ROW('Un-reviewed Change Requests')).contains('Change Request').should('exist'); + // Verify the created CR appears in My Un-reviewed Change Requests + cy.get(CR_ROW('My Un-reviewed Change Requests')).contains('Change Request').should('exist'); }; diff --git a/yarn.lock b/yarn.lock index bfdbdc8dcd..79d2a01d2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,15 +12,15 @@ __metadata: languageName: node linkType: hard -"@algolia/abtesting@npm:1.15.2": - version: 1.15.2 - resolution: "@algolia/abtesting@npm:1.15.2" +"@algolia/abtesting@npm:1.15.1": + version: 1.15.1 + resolution: "@algolia/abtesting@npm:1.15.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 4b8480a0bfa4463a208519dc4a788b27b8eb8a33b133a7dd1e4e26e37bb937ac0f02cf2fb45616ca63c50c207399c125d6ce9e65f836f5830dd0f8dbb016a719 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 81695b13c9c0ae9e36f677f0a13b9a50f6aab263d8ebc45f039af6f2a601cda78860790016121779aff62f196a9c31adb0bcfe7f6a126f5b68a1f9111bfbaa7b languageName: node linkType: hard @@ -55,82 +55,82 @@ __metadata: languageName: node linkType: hard -"@algolia/client-abtesting@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-abtesting@npm:5.49.2" +"@algolia/client-abtesting@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-abtesting@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 3a376c7e105b79d1b34a59f20e3c7529989c1670984aa6ce3e3f990c37fcf037a75b7520d54f329239da1a61f7882225e887dc9f09e265dc5489b25046b03627 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 943e70dc6b48148fc49d0eb96be84049194768ff0726f12aedd6265b04283142a4068f988b82b7f9969c77bf4a9ae92b73ccd71a2584c904509ac9ba8a8c7704 languageName: node linkType: hard -"@algolia/client-analytics@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-analytics@npm:5.49.2" +"@algolia/client-analytics@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-analytics@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 7c9826f9015dc8cd929664af00f1e805f52afb2138a7d6f5e28d4fa7ecab891715d8666abb4802a1b1775f7b5159efa7f39285459b4b97760fab303058a69513 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: b3174508f84e82342fc1b6c9b762af4b1a783fbd8e52a2f62a97a1c13bd9b076ada103ef09e71a958a90feefee572a57a1cc9f2a102811a7648edae8bf75729b languageName: node linkType: hard -"@algolia/client-common@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-common@npm:5.49.2" - checksum: 36424895c1bf2ec495c62bbfed9e8298d6fa0657c195d8310d1c26b89051a6357179637da6757233ef52ae48061fcd1cb6c0e7271dbfad928dd77ea591da5455 +"@algolia/client-common@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-common@npm:5.49.1" + checksum: eca24e5f3f9a581d76db0e8c432f8e767876813e4133697fb2c3c44e9c7d09b4827dd68caf6d299ebb25f995afc3933cd848e828f7fc17dfff6129a8096f64e1 languageName: node linkType: hard -"@algolia/client-insights@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-insights@npm:5.49.2" +"@algolia/client-insights@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-insights@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 756070df1d7ee804c3977c6c91ff2dd12fdbfab8bb6e09e2e3ce07fa6b79afc37e0e277da8e99d3597426029414da7de82455d4e1a743646a6b136263f29f75b + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 98d0721e933f1a3530cf847dcaff7b97b17bbea78bddc450cd7bdf8e22bd75907bbedb69fe3e101246d52c8a0ca0b1a8559579d46ea52e4f2e0941ec5a21f2d8 languageName: node linkType: hard -"@algolia/client-personalization@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-personalization@npm:5.49.2" +"@algolia/client-personalization@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-personalization@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: fca65deaefe98f6a2900861a2b0a7d0feb90f7fabb872fb6b0bfa4222b8a7534f7cd85d57eed847e414df5018c4313f2b95de13b37bdaa593254f7e6754d3d58 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: c2732e96522697d470ed769cbf8d32877c3a0bf55c65b634f1f5a9171642d8117ff719bb8fd26f312083f2fc02e3eec5fffcd3ce4e2e91401387a3c3121cf852 languageName: node linkType: hard -"@algolia/client-query-suggestions@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-query-suggestions@npm:5.49.2" +"@algolia/client-query-suggestions@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-query-suggestions@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 292b344793cde859799caacf497f9794968edba33b71803718f90002263737ffc881144f32e3d37d38c714986ae3e2e7ca41febca70bc4ea875706518ac45491 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 86d71d88ea50f6673c2a219818108875922bc170e8421f493f1c80f7587f365e4d123a8b3079f897d9a9c7548838112cded195c4c6f7719d8cd9d46563e5de88 languageName: node linkType: hard -"@algolia/client-search@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/client-search@npm:5.49.2" +"@algolia/client-search@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/client-search@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: c0362fd0260cfbbed35f4367aaafe1fb232fe9bd0c6eacf9aec205f9197c07efe5fc833e92229c59d1c3285010b18471df71da2fc698cce861b214d317bca687 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: b91de335754e45d457df97e87934ad6d0e41c034364820202d60fdc66ab2147ba4a378289a45cfd85fb2f9c32dd4e8ffc573fd4c8a472d497b4b234c73114361 languageName: node linkType: hard @@ -141,66 +141,66 @@ __metadata: languageName: node linkType: hard -"@algolia/ingestion@npm:1.49.2": - version: 1.49.2 - resolution: "@algolia/ingestion@npm:1.49.2" +"@algolia/ingestion@npm:1.49.1": + version: 1.49.1 + resolution: "@algolia/ingestion@npm:1.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: b1f1aa60b8c5355f2ae934ccab2695178b618c2ec6918737824bdf446e82b09e34294a56bb1a9fe1db63bcbb93cc27dedb576197bddf804de25a367350043540 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 54d72473fe8060e3490d671438d28422cb44da600a8b373896674df129ea616873fc75fd0734b259ceb58b1756f357da693d6363acd9ece624eb0dceed91f6a8 languageName: node linkType: hard -"@algolia/monitoring@npm:1.49.2": - version: 1.49.2 - resolution: "@algolia/monitoring@npm:1.49.2" +"@algolia/monitoring@npm:1.49.1": + version: 1.49.1 + resolution: "@algolia/monitoring@npm:1.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: adbea04499b89b2f248bf583d50939606b623872b0b86f63bd852f68908479530fff591163f5fdf9cbf598f9348d08667ee7862aaf929a2b3748427d8fe0390f + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 10b7f0a343dca2443a550f2023d1da0218ac8e3e2946cb3e79544600745e19555d88e9f4743d533a869a44f4e867cfd06d6458b78885346bcc260a2d16fe1027 languageName: node linkType: hard -"@algolia/recommend@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/recommend@npm:5.49.2" +"@algolia/recommend@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/recommend@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: f94f1caebc16278c497bbec63ec0735a3f5988b1f3664e441faa0eb81020ef12faae34b89fcb24a357adf692ba47320e639c3a51314b907c3b53cda7f4f85724 + "@algolia/client-common": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 54bf709f56ddc02314e740f074b24f34f9b448ba50dacc6900c774d33322481f23da98fda18fbc431b8853123ee7ab5389bbd99b7ab0def1b2aca6fc60beff75 languageName: node linkType: hard -"@algolia/requester-browser-xhr@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/requester-browser-xhr@npm:5.49.2" +"@algolia/requester-browser-xhr@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/requester-browser-xhr@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - checksum: 81414a4635296dc76aacc12a35fa40a07fadff38185608f9d48edc9758315a0ab59b64f7683159c18f70e331fa707e925c174d6aa30b42e1dc90a356daed9355 + "@algolia/client-common": 5.49.1 + checksum: d4de5a168f0c5f65e170b69e5cbb7d8b41a02417c929f81edcbbbf27ee3fde161a7374904a23fb26462e2edc19a52c02c16900200599dae3ebf0df111ec96f38 languageName: node linkType: hard -"@algolia/requester-fetch@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/requester-fetch@npm:5.49.2" +"@algolia/requester-fetch@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/requester-fetch@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - checksum: b5f3f4bbb27e9d61c0143d08f099429f34863d1dbb09457d99e15b370cab7c6450f5ef8626a20608e4c886c5d46b969d11ad21e821aad5c1715e35235197cc2c + "@algolia/client-common": 5.49.1 + checksum: dcc3db628c8b46c5405142347999039ef649d6612718fd95e1b4627798f95824da39e26b3d19f3c14a075324812669d710f06e6802967ebd35a0836e2fc197ee languageName: node linkType: hard -"@algolia/requester-node-http@npm:5.49.2": - version: 5.49.2 - resolution: "@algolia/requester-node-http@npm:5.49.2" +"@algolia/requester-node-http@npm:5.49.1": + version: 5.49.1 + resolution: "@algolia/requester-node-http@npm:5.49.1" dependencies: - "@algolia/client-common": 5.49.2 - checksum: 968e058023fee6320fe73531067ba05c36d11da31d6754ce003392cfd7b34b05ec8e35e035895d4fc2d60c5ccb3df5a489bb6d2908e6f9e47f492e1dcb77485a + "@algolia/client-common": 5.49.1 + checksum: 4840b4453326375200a979c3786829e0266df49e9b0bc9c3f19f68a76bf75ce6977ef808555fc01a308f0e0dac973a33e81b421dbcf6dc7863ca9c0eabf6019a languageName: node linkType: hard @@ -353,9 +353,9 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.7": - version: 0.6.7 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.7" +"@babel/helper-define-polyfill-provider@npm:^0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.6": + version: 0.6.6 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.6" dependencies: "@babel/helper-compilation-targets": ^7.28.6 "@babel/helper-plugin-utils": ^7.28.6 @@ -364,7 +364,7 @@ __metadata: resolve: ^1.22.11 peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 203518ad1c6a4c2e6805309168de2d08a9e6099f407d28d4999ef1ce2130563a3eecb67ea9c42309912d92c57ca22cf5eddcf8881fa1351b8738ecb34edc90d4 + checksum: 582efe522e7ef75228f7eeea63fd659567ce865365e3d4b9d94451825114a7f1c8b61791bbbf134aa1b2aa6ee37620b145e74879dace7568107057180153e72e languageName: node linkType: hard @@ -5215,9 +5215,9 @@ __metadata: languageName: node linkType: hard -"@mui/types@npm:^7.2.15, @mui/types@npm:^7.2.17, @mui/types@npm:^7.4.12": - version: 7.4.12 - resolution: "@mui/types@npm:7.4.12" +"@mui/types@npm:^7.2.15, @mui/types@npm:^7.2.17, @mui/types@npm:^7.4.11": + version: 7.4.11 + resolution: "@mui/types@npm:7.4.11" dependencies: "@babel/runtime": ^7.28.6 peerDependencies: @@ -5225,7 +5225,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: f3217cc0a7beaf7f5e6557e5a55c8597d7a7bfc42f69472ac0fe2de6aeee6e7d85828f05a09ccc31de1261e713cf4203f597cd6b839e269829b534df2109ad98 + checksum: 95fb7622b34034b6601193e515174194ff1d211447ffdf52a69968a9c492866313aafc540338770c2d119c9797c966ab3788cd9b630ff74efccaf64125722be3 languageName: node linkType: hard @@ -5282,11 +5282,11 @@ __metadata: linkType: hard "@mui/utils@npm:^5.16.6 || ^6.0.0 || ^7.0.0": - version: 7.3.9 - resolution: "@mui/utils@npm:7.3.9" + version: 7.3.8 + resolution: "@mui/utils@npm:7.3.8" dependencies: "@babel/runtime": ^7.28.6 - "@mui/types": ^7.4.12 + "@mui/types": ^7.4.11 "@types/prop-types": ^15.7.15 clsx: ^2.1.1 prop-types: ^15.8.1 @@ -5297,7 +5297,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 13d4271081800f73131a43adc8b2e7144a7045e6d92f12265d61929fd319e822ce71a0947962583b98fe83ba8a393417d82e3bbe57cba5d3ba0df1e88a7377c9 + checksum: 0155a38f3139b3331f5b3a8c10377bb9c7106821fc6db34c2c3253b92ab80b23349c8e2b6f2ce9ff8d99b7855b69447a0de57dbcfd7931dad4141ab74d460094 languageName: node linkType: hard @@ -7371,12 +7371,26 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=18.0.0, @types/node@npm:^25.0.0": - version: 25.4.0 - resolution: "@types/node@npm:25.4.0" +"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=18.0.0": + version: 25.3.3 + resolution: "@types/node@npm:25.3.3" dependencies: undici-types: ~7.18.0 - checksum: 793bf88ecb62ae3be0929c5348181680e95ae10bc94084a08097a6b87b7c511c77f8c191687bf6a75af4bd79c4fae727d1680d8f6d02f3f8c751c1baa7d8e507 + checksum: 9186aae36f8ddb0b3630dba446e5c16e5f3e6c5e7a4708d117a394d3e3b6f41db2dd83a6127adf4567826776a732ca9e2561594667bce74bb18ea4d59ee1e06a + languageName: node + linkType: hard + +"@types/node@npm:18.17.1": + version: 18.17.1 + resolution: "@types/node@npm:18.17.1" + checksum: 56201bda9a2d05d68602df63b4e67b0545ac8c6d0280bd5fb31701350a978a577a027501fbf49db99bf177f2242ebd1244896bfd35e89042d5bd7dfebff28d4e + languageName: node + linkType: hard + +"@types/node@npm:20.0.0": + version: 20.0.0 + resolution: "@types/node@npm:20.0.0" + checksum: 7dadc41081eee634fd0b19e46dfb0a74f8296ee562533118f7c2f2b54dcaba7124961d506db425fff74d8e8288610b93939cd06fa723f053c72b1a7e87aa4ddc languageName: node linkType: hard @@ -7387,6 +7401,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.0.0": + version: 20.19.35 + resolution: "@types/node@npm:20.19.35" + dependencies: + undici-types: ~6.21.0 + checksum: 465aac31b208bd4248f64b05abd0218509c696c55f62ae000571427e357168650d4c484f4531c37ad3be2a75198ba13f9dda7a0c14c539b09cf52b529d7f0d66 + languageName: node + linkType: hard + "@types/nodemailer@npm:^6.4.0": version: 6.4.23 resolution: "@types/nodemailer@npm:6.4.23" @@ -7439,9 +7462,9 @@ __metadata: linkType: hard "@types/qs@npm:*": - version: 6.15.0 - resolution: "@types/qs@npm:6.15.0" - checksum: 871162881f1c83e61d0c8c243c65549be5dddf33a6911f3324edeebd4087207b1174644da9a3afaa20cf494c5288d2a1ece09e10e4822f755339f14a05c339ea + version: 6.14.0 + resolution: "@types/qs@npm:6.14.0" + checksum: 1909205514d22b3cbc7c2314e2bd8056d5f05dfb21cf4377f0730ee5e338ea19957c41735d5e4806c746176563f50005bbab602d8358432e25d900bdf4970826 languageName: node linkType: hard @@ -8552,24 +8575,24 @@ __metadata: linkType: hard "algoliasearch@npm:^5.37.0": - version: 5.49.2 - resolution: "algoliasearch@npm:5.49.2" - dependencies: - "@algolia/abtesting": 1.15.2 - "@algolia/client-abtesting": 5.49.2 - "@algolia/client-analytics": 5.49.2 - "@algolia/client-common": 5.49.2 - "@algolia/client-insights": 5.49.2 - "@algolia/client-personalization": 5.49.2 - "@algolia/client-query-suggestions": 5.49.2 - "@algolia/client-search": 5.49.2 - "@algolia/ingestion": 1.49.2 - "@algolia/monitoring": 1.49.2 - "@algolia/recommend": 5.49.2 - "@algolia/requester-browser-xhr": 5.49.2 - "@algolia/requester-fetch": 5.49.2 - "@algolia/requester-node-http": 5.49.2 - checksum: 8fded60fbb38bdf6884f9b0999cc8dcb57ac458d25043a479275a040b5ce1493f21445617c6188ed861a2240fbfe545c12f11f4ead02ed79394ea663b5bc87f6 + version: 5.49.1 + resolution: "algoliasearch@npm:5.49.1" + dependencies: + "@algolia/abtesting": 1.15.1 + "@algolia/client-abtesting": 5.49.1 + "@algolia/client-analytics": 5.49.1 + "@algolia/client-common": 5.49.1 + "@algolia/client-insights": 5.49.1 + "@algolia/client-personalization": 5.49.1 + "@algolia/client-query-suggestions": 5.49.1 + "@algolia/client-search": 5.49.1 + "@algolia/ingestion": 1.49.1 + "@algolia/monitoring": 1.49.1 + "@algolia/recommend": 5.49.1 + "@algolia/requester-browser-xhr": 5.49.1 + "@algolia/requester-fetch": 5.49.1 + "@algolia/requester-node-http": 5.49.1 + checksum: 0885136a9d072ab35fa2a28c773c217f9c4c97b68ddee2decdb84bb98d26f831ebde7dfad1a9504838b9ab365cc28aef78a9b5967ce9a423ce1a3cd030f0b862 languageName: node linkType: hard @@ -9126,15 +9149,15 @@ __metadata: linkType: hard "babel-plugin-polyfill-corejs2@npm:^0.4.14, babel-plugin-polyfill-corejs2@npm:^0.4.15": - version: 0.4.16 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.16" + version: 0.4.15 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.15" dependencies: "@babel/compat-data": ^7.28.6 - "@babel/helper-define-polyfill-provider": ^0.6.7 + "@babel/helper-define-polyfill-provider": ^0.6.6 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 54e493a945a21f8454805f2f930f8e91580e6fbcda31c8a28366a37e9351aad7460a95d9534244496b2c2cb0d5af95c2caa237502557c4b75dcb2c47c1a4220d + checksum: cf32e00ee54cdd75a3acec408f3467edc20cff4359c2bc5fb221144a489d6c0d5936031e18d66483613194a7012034b8a9e1237b84e9063f963f352efc1558bc languageName: node linkType: hard @@ -9151,25 +9174,25 @@ __metadata: linkType: hard "babel-plugin-polyfill-corejs3@npm:^0.14.0": - version: 0.14.1 - resolution: "babel-plugin-polyfill-corejs3@npm:0.14.1" + version: 0.14.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.0" dependencies: - "@babel/helper-define-polyfill-provider": ^0.6.7 + "@babel/helper-define-polyfill-provider": ^0.6.6 core-js-compat: ^3.48.0 peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 4bd5decd23f47c5148fdbf73ed959187eeddd5d964707b6278e6f0dc20d9131af5f6ee8812098a363c7ee19168fede3fd7d56ecba682f08bcf8be20071e26d1e + checksum: dda87e15dd4e36e989fafe3719d9e67ad1ebcfae3530d1b46f285439ecdd1709b147d7a656b10091b37f6490630836fe454755bc8f829d237ada1ac44603ff81 languageName: node linkType: hard "babel-plugin-polyfill-regenerator@npm:^0.6.5, babel-plugin-polyfill-regenerator@npm:^0.6.6": - version: 0.6.7 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.7" + version: 0.6.6 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.6" dependencies: - "@babel/helper-define-polyfill-provider": ^0.6.7 + "@babel/helper-define-polyfill-provider": ^0.6.6 peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 40640a7caa6a7af07fcbedda446c00c057096dc12c142d304cc987af6a2611ee99a9693abdb7c98eccff6889fe9ef352981add970435805f85b9664998bbd416 + checksum: 8de7ea32856e75784601cacf8f4e3cbf04ce1fd05d56614b08b7bbe0674d1e59e37ccaa1c7ed16e3b181a63abe5bd43a1ab0e28b8c95618a9ebf0be5e24d6b25 languageName: node linkType: hard @@ -9255,7 +9278,7 @@ __metadata: "@types/express-jwt": ^6.0.4 "@types/jsonwebtoken": ^8.5.9 "@types/multer": ^1.4.7 - "@types/node": ^25.0.0 + "@types/node": ^20.0.0 "@types/nodemailer": ^6.4.0 "@types/supertest": ^2.0.12 body-parser: ^1.19.0 @@ -9809,9 +9832,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001759, caniuse-lite@npm:^1.0.30001774": - version: 1.0.30001777 - resolution: "caniuse-lite@npm:1.0.30001777" - checksum: 962e97beb3a21d84ee7b5a04d68b5dc55cd3a84ff14f1ec6ad031a76ade8432a2d5d4a9697099d2f7e197f707d4f2698a01b756c8c68a30a13b37ded09c9b58a + version: 1.0.30001776 + resolution: "caniuse-lite@npm:1.0.30001776" + checksum: 78df45dfce9162b218c766605a0184d8478f8aa6165bb21aa840e5ce5066332d34c6823bcd12e604887b4e221bb4abde195d62d5023bd30f89d089c5896e0bcc languageName: node linkType: hard @@ -12242,8 +12265,8 @@ __metadata: linkType: hard "es-iterator-helpers@npm:^1.2.1": - version: 1.3.0 - resolution: "es-iterator-helpers@npm:1.3.0" + version: 1.2.2 + resolution: "es-iterator-helpers@npm:1.2.2" dependencies: call-bind: ^1.0.8 call-bound: ^1.0.4 @@ -12260,9 +12283,8 @@ __metadata: has-symbols: ^1.1.0 internal-slot: ^1.1.0 iterator.prototype: ^1.1.5 - math-intrinsics: ^1.1.0 safe-array-concat: ^1.1.3 - checksum: 5563aa318f588b08490e53f1cb6e83807a56fbfc51fbde5a0dd72ede0624fb14bb34244e534bd91be04be554bfc739135ca146414df959d0b033b840bca94810 + checksum: 33e148b592d41630ea53b20ec8d6f2ca7516871c43bdf1619fdb4c770361c625f134ff4276332d6e08e9f59d1cd75532a74723f56176c4599e0387f51750e286 languageName: node linkType: hard @@ -13871,7 +13893,7 @@ __metadata: "@types/canvas-confetti": ^1.9.0 "@types/jest": ^29.5.14 "@types/multer": ^1.4.12 - "@types/node": ^25.0.0 + "@types/node": 20.0.0 "@typescript-eslint/eslint-plugin": 8.20.0 "@typescript-eslint/parser": 8.20.0 canvas-confetti: ^1.9.3 @@ -13919,9 +13941,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.4.1 - resolution: "flatted@npm:3.4.1" - checksum: c98e458fac822c3d6f814e38154f9f5c7aa4b10c259467beda05740596cf9a826754a7a77b840b8ca2926a3e22a6f3a27992f0c673a9dbb4f6ea9f132e9a9db4 + version: 3.3.4 + resolution: "flatted@npm:3.3.4" + checksum: 67f1da2949782ead8cb6846b08b941c9569a8232bf43f1cd754736473400377b24b371939a251095c6df59f99a9dd667552b2008ae252d1b0a821ef5ecb5498d languageName: node linkType: hard @@ -14090,7 +14112,7 @@ __metadata: "@testing-library/react-hooks": ^8.0.1 "@testing-library/user-event": ^14.6.0 "@types/file-saver": ^2.0.5 - "@types/node": ^25.0.0 + "@types/node": 20.0.0 "@types/react": ^19.0.7 "@types/react-dom": ^19.0.3 "@types/react-helmet": ^6.1.6 @@ -15387,7 +15409,7 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^5.1.5": +"immutable@npm:^5.0.2": version: 5.1.5 resolution: "immutable@npm:5.1.5" checksum: 6aa0ed4ba7eac225982fea7adbee16e939744e9d20bf15e0f5d4561d7fa2fe69aee6e63ccdb0dffa22e012f6a690ee5359783471ecd0782de6125f2214de3dd8 @@ -17063,13 +17085,13 @@ __metadata: linkType: hard "jsonpath@npm:^1.1.1": - version: 1.3.0 - resolution: "jsonpath@npm:1.3.0" + version: 1.2.1 + resolution: "jsonpath@npm:1.2.1" dependencies: esprima: 1.2.5 static-eval: 2.1.1 underscore: 1.13.6 - checksum: 7639c5cf8a1af9cdff7cc1ed8dce9f8e3d65104cf41d781034f192aecc32d1e54e9cdbf865aa5bff888a48d55dbb92095e2f5986e844e82bacf4f632c59cf288 + checksum: 559e8bc5e559f9dfb0010c5ce555995f5bde08293ac73d2e715dcfe488f2669b5885fd848f23493d1731bf88cf429a857a9a30917fd90aeccb1745b1ed269e50 languageName: node linkType: hard @@ -18647,14 +18669,14 @@ __metadata: linkType: hard "mini-css-extract-plugin@npm:^2.4.5, mini-css-extract-plugin@npm:^2.9.2": - version: 2.10.1 - resolution: "mini-css-extract-plugin@npm:2.10.1" + version: 2.10.0 + resolution: "mini-css-extract-plugin@npm:2.10.0" dependencies: schema-utils: ^4.0.0 tapable: ^2.2.1 peerDependencies: webpack: ^5.0.0 - checksum: da1aa2b058d238f364022ea00d242e6c1fa398e6dd38d1c1e0d960aa66cdd0f070afc85b4d6e0df6febe8b48e6ee19d325250460ae0e3a2aa365837ca20b7af2 + checksum: 53396dcf7ecf9706cc9d2a9fe5289e4c740b0f06978a9576b39fa973f54a69c7ccab33997a3bfa801608629c48d2c71dbcb735cf858780792bd4322779692c21 languageName: node linkType: hard @@ -18986,11 +19008,11 @@ __metadata: linkType: hard "node-abi@npm:^3.3.0": - version: 3.88.0 - resolution: "node-abi@npm:3.88.0" + version: 3.87.0 + resolution: "node-abi@npm:3.87.0" dependencies: semver: ^7.3.5 - checksum: 0570ad00595eaaa4364125fb1c8db7e64da944b882fa18a946c288da64827650afa97f3a65e12467f04ebac2b8ba325c26686f6c77113b9077b987f483d130a1 + checksum: ffe24d2e9e9fcf46c9aff7ddd93cbd5b128ce0a7a4032019ce2eeef3d5fad34cfc7f48650e3051fc87bb28621f6d2be166d0a19135ba80d39182897cb4bd29e1 languageName: node linkType: hard @@ -23478,19 +23500,19 @@ __metadata: linkType: hard "sass@npm:^1.54.0": - version: 1.98.0 - resolution: "sass@npm:1.98.0" + version: 1.97.3 + resolution: "sass@npm:1.97.3" dependencies: "@parcel/watcher": ^2.4.1 chokidar: ^4.0.0 - immutable: ^5.1.5 + immutable: ^5.0.2 source-map-js: ">=0.6.2 <2.0.0" dependenciesMeta: "@parcel/watcher": optional: true bin: sass: sass.js - checksum: d47220069f506703437c1ecc7d18049de7b4075eea66d734be7ee58e85ad426bfdc305683cff6673e65c14f6f315723d769fe40554cf6b02cb11e758b0b888b4 + checksum: 781c1c8179453b048f0cb9f5e3b89ded34727ddc8203fbe9b630fe266dac6c001d497eaba9eeadf701eeb55c86d8e23c609732c9cc95f88815554561d26733ef languageName: node linkType: hard @@ -23854,7 +23876,7 @@ __metadata: version: 0.0.0-use.local resolution: "shared@workspace:src/shared" dependencies: - "@types/node": ^25.0.0 + "@types/node": 18.17.1 dayjs: ^1.11.19 ts-node: ^8.10.1 typescript: ^5.7.3 @@ -24898,15 +24920,15 @@ __metadata: linkType: hard "tar@npm:^7.5.4": - version: 7.5.11 - resolution: "tar@npm:7.5.11" + version: 7.5.10 + resolution: "tar@npm:7.5.10" dependencies: "@isaacs/fs-minipass": ^4.0.0 chownr: ^3.0.0 minipass: ^7.1.2 minizlib: ^3.1.0 yallist: ^5.0.0 - checksum: 7f6785a85dd571b88985e493ec86f692962cbfa7b4017961fddfd2241e0ff3bcd89ed347f4c02b5433aa22b30cca5566e8711543df054fda8fd12425f505378f + checksum: aed1a7ae188fc80539184682bfaed7c4d5ae276f591dce67cc03b4ed8898aebde0cc195187f6abd455e3f25b24399a809ed2eaf6410ca3abc1ba30b19a94089e languageName: node linkType: hard @@ -24940,8 +24962,8 @@ __metadata: linkType: hard "terser-webpack-plugin@npm:^5.2.5, terser-webpack-plugin@npm:^5.3.17, terser-webpack-plugin@npm:^5.3.9": - version: 5.4.0 - resolution: "terser-webpack-plugin@npm:5.4.0" + version: 5.3.17 + resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: "@jridgewell/trace-mapping": ^0.3.25 jest-worker: ^27.4.5 @@ -24956,7 +24978,7 @@ __metadata: optional: true uglify-js: optional: true - checksum: 12b7b356aca6808707f798a0e0f504a4697f1089d221b5f804afba627f1b9773548ec941a37bd9905e906403ebfe9bc0a2d056c91ffc1f2dc638feb88ab7d8f7 + checksum: 182631872b82c98240b37af150d289178ac9891892cc04d092661760e702e24428dac6f9285c6dd40b0a15ca508979470d5c498f4e350a5ec79a7df5463aa492 languageName: node linkType: hard @@ -25120,21 +25142,21 @@ __metadata: languageName: node linkType: hard -"tldts-core@npm:^7.0.25": - version: 7.0.25 - resolution: "tldts-core@npm:7.0.25" - checksum: d441cbfa87b85af9d0ef9ff9a9eeceb4c04d2b068b8971399f9704e03ce0b18c3ffadcf22cf64fba4ce6c7d7923f2a0d015950a37bcdedbe75953c3547417548 +"tldts-core@npm:^7.0.24": + version: 7.0.24 + resolution: "tldts-core@npm:7.0.24" + checksum: bd70535996dbd815674a8178e93b886f04347acf456ad01b3253a8a3ea9dd5d4952b6339df3ccb855b7f5a4c6f7934d58abfb7ba75150e75195297c1f74984a1 languageName: node linkType: hard "tldts@npm:^7.0.5": - version: 7.0.25 - resolution: "tldts@npm:7.0.25" + version: 7.0.24 + resolution: "tldts@npm:7.0.24" dependencies: - tldts-core: ^7.0.25 + tldts-core: ^7.0.24 bin: tldts: bin/cli.js - checksum: da7b28f590246f1d0b1eecdec8a463b1af54c67523c29c846ab5a6832ce1e5342f710071def71bc395d73da11d76f48a7d22a90177a8546dee6b047119da44d7 + checksum: 684e65d9ecf7a7a3b7db3d533d358eee9127f9664459583be55e1193e2bff15a39adc5a66e311df987d6065b656e48bf4cea1f87344649403976d390a597fbc5 languageName: node linkType: hard @@ -25667,6 +25689,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 46331c7d6016bf85b3e8f20c159d62f5ae471aba1eb3dc52fff35a0259d58dcc7d592d4cc4f00c5f9243fa738a11cfa48bd20203040d4a9e6bc25e807fab7ab3 + languageName: node + linkType: hard + "undici-types@npm:~7.18.0": version: 7.18.2 resolution: "undici-types@npm:7.18.2"