diff --git a/src/backend/src/controllers/change-requests.controllers.ts b/src/backend/src/controllers/change-requests.controllers.ts index 208937bac8..3618191093 100644 --- a/src/backend/src/controllers/change-requests.controllers.ts +++ b/src/backend/src/controllers/change-requests.controllers.ts @@ -23,6 +23,15 @@ export default class ChangeRequestsController { } } + static async getAllGuestChangeRequests(req: Request, res: Response, next: NextFunction) { + try { + const changeRequests = await ChangeRequestsService.getAllGuestChangeRequests(req.organization); + res.status(200).json(changeRequests); + } catch (error: unknown) { + next(error); + } + } + static async getToReviewChangeRequests(req: Request, res: Response, next: NextFunction) { try { const changeRequests = await ChangeRequestsService.getToReviewChangeRequests(req.currentUser, req.organization); 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 abae733c5d..ebe4c8be15 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,6 +68,51 @@ 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/schema.prisma b/src/backend/src/prisma/schema.prisma index 2a084a2df0..47adf8f79e 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -739,11 +739,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]) diff --git a/src/backend/src/routes/change-requests.routes.ts b/src/backend/src/routes/change-requests.routes.ts index 89b3fe0c19..744f4f615c 100644 --- a/src/backend/src/routes/change-requests.routes.ts +++ b/src/backend/src/routes/change-requests.routes.ts @@ -14,6 +14,7 @@ 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 2237b8ac42..8f86e1a428 100644 --- a/src/backend/src/routes/recruitment.routes.ts +++ b/src/backend/src/routes/recruitment.routes.ts @@ -51,7 +51,7 @@ recruitmentRouter.post( recruitmentRouter.delete('/faq/:faqId/delete', RecruitmentController.deleteFaq); recruitmentRouter.post( - '/guestDefinition/create', + '/guestdefinition/create', nonEmptyString(body('term')), nonEmptyString(body('description')), body('order').isInt(), @@ -62,6 +62,6 @@ recruitmentRouter.post( RecruitmentController.createGuestDefinition ); -recruitmentRouter.get('/guestDefinitions', RecruitmentController.getAllGuestDefintions); +recruitmentRouter.get('/guestdefinitions', RecruitmentController.getAllGuestDefintions); export default recruitmentRouter; diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 27202c9c16..58bb0b3f77 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -28,7 +28,10 @@ import { DeletedException, InvalidOrganizationException } from '../utils/errors.utils.js'; -import changeRequestTransformer, { changeRequestManyTransformer } from '../transformers/change-requests.transformer.js'; +import changeRequestTransformer, { + changeRequestManyTransformer, + guestChangeRequestTransformer +} from '../transformers/change-requests.transformer.js'; import { allChangeRequestsReviewed, validateProposedChangesFields, @@ -55,11 +58,13 @@ 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 { /** @@ -97,6 +102,20 @@ export default class ChangeRequestsService { 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 * diff --git a/src/backend/src/transformers/change-requests.transformer.ts b/src/backend/src/transformers/change-requests.transformer.ts index 1c39c2f040..63f6723c88 100644 --- a/src/backend/src/transformers/change-requests.transformer.ts +++ b/src/backend/src/transformers/change-requests.transformer.ts @@ -10,7 +10,8 @@ import { WorkPackageStage, BudgetChangeRequest, isWorkPackageWbs, - LeadershipChangeRequest + LeadershipChangeRequest, + ChangeRequestStatus } from 'shared'; import { wbsNumOf } from '../utils/utils.js'; import { calculateChangeRequestStatus, convertCRScopeWhyType } from '../utils/change-requests.utils.js'; @@ -25,10 +26,12 @@ 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 @@ -229,3 +232,41 @@ 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/frontend/src/apis/change-requests.api.ts b/src/frontend/src/apis/change-requests.api.ts index 7f101a45de..9892caddeb 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 } from 'shared'; +import { ChangeRequest, WbsNumber, ChangeRequestType, GuestChangeRequest } from 'shared'; import { apiUrls } from '../utils/urls'; import { changeRequestTransformer } from './transformers/change-requests.transformers'; import { CreateStandardChangeRequestPayload } from '../hooks/change-requests.hooks'; @@ -18,6 +18,12 @@ 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/hooks/change-requests.hooks.ts b/src/frontend/src/hooks/change-requests.hooks.ts index 84061fac51..0febdf9fc5 100644 --- a/src/frontend/src/hooks/change-requests.hooks.ts +++ b/src/frontend/src/hooks/change-requests.hooks.ts @@ -12,7 +12,8 @@ import { ProposedSolutionCreateArgs, WbsNumber, WorkPackageProposedChangesCreateArgs, - LeadershipChangeCreateArgs + LeadershipChangeCreateArgs, + GuestChangeRequest } from 'shared'; import { createActivationChangeRequest, @@ -28,7 +29,8 @@ import { getUnreviewedChangeRequests, getApprovedChangeRequests, createBudgetChangeRequest, - createLeadershipChangeRequest + createLeadershipChangeRequest, + getAllGuestChangeRequests } from '../apis/change-requests.api'; /** @@ -41,6 +43,13 @@ export const useAllChangeRequests = () => { }); }; +export const useAllGuestChangeRequests = () => { + return useQuery(['guest change requests'], async () => { + const { data } = await getAllGuestChangeRequests(); + return data; + }); +}; + export const useGetToReviewChangeRequests = () => { return useQuery(['change requests', 'to-review'], async () => { const { data } = await getToReviewChangeRequests(); diff --git a/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx b/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx index c6b22d0b05..6105a215bf 100644 --- a/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx +++ b/src/frontend/src/pages/ChangeRequestsPage/ChangeRequestsView.tsx @@ -9,6 +9,7 @@ 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(); @@ -17,6 +18,9 @@ const ChangeRequestsView: React.FC = () => { // Default to the "overview" tab const [tabIndex, setTabIndex] = useState(0); + if (isGuest(user.role)) { + return ; + } const headerRight = ( { + 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/utils/urls.ts b/src/frontend/src/utils/urls.ts index 5570d80e33..07493fd150 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -120,6 +120,7 @@ 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)}` : ''); @@ -605,6 +606,7 @@ export const apiUrls = { homePageWorkPackages, changeRequests, + guestChangeRequests, changeRequestsById, changeRequestsReview, changeRequestDelete, diff --git a/src/shared/src/types/change-request-types.ts b/src/shared/src/types/change-request-types.ts index a57b56b354..1b1f2b9060 100644 --- a/src/shared/src/types/change-request-types.ts +++ b/src/shared/src/types/change-request-types.ts @@ -64,6 +64,19 @@ 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;