From e22f234ad3c046c167b6bbe8b2def1338f6fb259 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Wed, 28 Jan 2026 21:46:14 -0500 Subject: [PATCH 1/9] #3907 frontend project leadership checks --- .../ProjectForm/ProjectEditContainer.tsx | 46 +++++++++++++++++++ .../ProjectForm/ProjectForm.tsx | 8 ++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx index 91c9a66c4a..5227684db2 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx @@ -42,6 +42,9 @@ const ProjectEditContainer: React.FC = ({ project, ex const { mutateAsync, isLoading } = useEditSingleProject(project.wbsNum); const { mutateAsync: mutateCRAsync, isLoading: isCRHookLoading } = useCreateStandardChangeRequest(); + // TODO: Create auto-approved leadership CR hook + // const { mutateAsync: mutateAutoApprovedCR, isLoading: isAutoApprovedLoading } = useCreateAutoApprovedLeadershipCR(); + const { data: allLinkTypes, isLoading: allLinkTypesIsLoading, @@ -163,6 +166,25 @@ const ProjectEditContainer: React.FC = ({ project, ex ) }); + // Check if only lead/manager changed + const checkOnlyLeadershipChanged = ( + formName: string, + formBudget: number, + formSummary: string, + formLinks: any[], + formDescriptionBullets: any[] + ) => { + return ( + formName === project.name && + formBudget === project.budget && + formSummary === project.summary && + JSON.stringify(formLinks.map((l) => `${l.linkTypeName}:${l.url}`).sort()) === + JSON.stringify(project.links.map((l) => `${l.linkType.name}:${l.url}`).sort()) && + JSON.stringify(formDescriptionBullets) === JSON.stringify(bulletsToObject(project.descriptionBullets)) && + (leadId !== project.lead?.userId.toString() || managerId !== project.manager?.userId.toString()) + ); + }; + const onSubmitChangeRequest = async (data: ProjectCreateChangeRequestFormInput) => { const { name, budget, summary, links, type, what, why, descriptionBullets } = data; @@ -199,6 +221,20 @@ const ProjectEditContainer: React.FC = ({ project, ex const { name, budget, summary, links, descriptionBullets, crId } = data; try { + const onlyLeadershipChanged = checkOnlyLeadershipChanged(name, budget, summary, links, descriptionBullets); + + if (onlyLeadershipChanged) { + const autoCRPayload = { + wbsNum: project.wbsNum, + projectId: project.id, + leadId, + managerId + }; + // await mutateAutoApprovedCR(autoCRPayload); + exitEditMode(); + return; + } + if (!crId) throw new Error('Change request id is required for editing project'); const payload: EditSingleProjectPayload = { @@ -221,6 +257,15 @@ const ProjectEditContainer: React.FC = ({ project, ex } }; + // calculate for submit button status + const onlyLeadershipChanged = checkOnlyLeadershipChanged( + defaultValues.name, + defaultValues.budget, + defaultValues.summary, + defaultValues.links, + defaultValues.descriptionBullets + ); + return ( = ({ project, ex 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 4052d429a2..dfcfc9fd2b 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx @@ -59,6 +59,7 @@ interface ProjectFormContainerProps { setCarNumber: (carNumber: number) => void; carNumber?: number; changeRequestFormReturn: ChangeRequestFormReturn; + onlyLeadershipChanged?: boolean; } const ProjectFormContainer: React.FC = ({ @@ -73,7 +74,8 @@ const ProjectFormContainer: React.FC = ({ managerId, onSubmitChangeRequest, setCarNumber, - changeRequestFormReturn + changeRequestFormReturn, + onlyLeadershipChanged }) => { const [isModalOpen, setIsModalOpen] = useState(false); let changeRequestFormInput: ChangeRequestFormInput | undefined = undefined; @@ -239,7 +241,7 @@ const ProjectFormContainer: React.FC = ({ variant="contained" onClick={() => setIsModalOpen(true)} sx={{ mx: 1 }} - disabled={changeRequestInputExists} + disabled={changeRequestInputExists || onlyLeadershipChanged} > Create Change Request @@ -249,7 +251,7 @@ const ProjectFormContainer: React.FC = ({ Cancel Date: Wed, 28 Jan 2026 22:10:10 -0500 Subject: [PATCH 2/9] #3907 wp frontend --- .../WorkPackageForm/EditWorkPackageForm.tsx | 5 ++ .../pages/WorkPackageForm/WorkPackageForm.tsx | 3 + .../WorkPackageForm/WorkPackageFormView.tsx | 61 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx b/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx index 7ed3267b42..db4acb5dc8 100644 --- a/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx +++ b/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx @@ -20,8 +20,12 @@ const EditWorkPackageForm: React.FC = ({ wbsNum, workP const { mutateAsync: editWorkPackage, isLoading } = useEditWorkPackage(wbsNum); const { mutateAsync: createWorkPackageScopeCR, isLoading: createStandardChangeRequestIsLoading } = useCreateStandardChangeRequest(); + // TODO: Create auto-approved leadership CR hook for work packages + // const { mutateAsync: createAutoApprovedWPLeadershipCR, isLoading: createAutoApprovedWPIsLoading } = + // useCreateAutoApprovedWPLeadershipCR(); if (isLoading || createStandardChangeRequestIsLoading) return ; + // if (isLoading || createStandardChangeRequestIsLoading || createAutoApprovedWPIsLoading) return ; const schema = yup.object().shape({ name: yup.string().required('Name is required!'), @@ -53,6 +57,7 @@ const EditWorkPackageForm: React.FC = ({ wbsNum, workP wbsNum={wbsNum} workPackageMutateAsync={editWorkPackageWrapper} createWorkPackageScopeCR={createWorkPackageScopeCR} + createAutoApprovedLeadershipCR={/*createAutoApprovedWPLeadershipCR*/ () => {}} exitActiveMode={() => { setPageMode(false); history.push(`${history.location.pathname}`); diff --git a/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx b/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx index e8b516d516..e633405050 100644 --- a/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx +++ b/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx @@ -16,6 +16,7 @@ interface WorkPackageFormProps { crId?: string; workPackageMutateAsync: (data: WorkPackageApiInputs) => void; createWorkPackageScopeCR: (data: CreateStandardChangeRequestPayload) => void; + createAutoApprovedLeadershipCR: (data: any) => void; // update this to auto approved cr payload schema: ObjectSchema; breadcrumbs: { name: string; route: string }[]; } @@ -24,6 +25,7 @@ const WorkPackageForm: React.FC = ({ wbsNum, workPackageMutateAsync, createWorkPackageScopeCR, + createAutoApprovedLeadershipCR, exitActiveMode, crId, schema, @@ -76,6 +78,7 @@ const WorkPackageForm: React.FC = ({ exitActiveMode={exitActiveMode} workPackageMutateAsync={workPackageMutateAsync} createWorkPackageScopeCR={createWorkPackageScopeCR} + createAutoApprovedLeadershipCR={createAutoApprovedLeadershipCR} defaultValues={defaultValues} wbsElement={wbsElement} leadOrManagerOptions={leadOrManagerOptions} diff --git a/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx b/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx index 231bf2722a..ffd96ff3b7 100644 --- a/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx +++ b/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx @@ -68,6 +68,7 @@ interface WorkPackageFormViewProps { exitActiveMode: () => void; workPackageMutateAsync: (data: WorkPackageApiInputs) => void; createWorkPackageScopeCR: (data: CreateStandardChangeRequestPayload) => void; + createAutoApprovedLeadershipCR: (data: any) => void; defaultValues?: WorkPackageFormViewPayload; wbsElement: WbsElement; leadOrManagerOptions: User[]; @@ -92,6 +93,7 @@ const WorkPackageFormView: React.FC = ({ exitActiveMode, workPackageMutateAsync, createWorkPackageScopeCR, + createAutoApprovedLeadershipCR, defaultValues, wbsElement, leadOrManagerOptions, @@ -223,10 +225,53 @@ const WorkPackageFormView: React.FC = ({ if (workPackageTemplateisLoading || !workPackageTemplates) return ; if (workPackageTemplateisError) return ; + // Check if only lead/manager changed + const checkOnlyLeadershipChanged = ( + formName: string, + formStartDate: Date, + formDuration: number, + formBlockedBy: string[], + formStage: string, + formDescriptionBullets: DescriptionBulletPreview[] + ) => { + if (!defaultValues) return false; // Only relevant for edits + + return ( + formName === defaultValues.name && + transformDate(formStartDate) === transformDate(defaultValues.startDate) && + formDuration === defaultValues.duration && + JSON.stringify(formBlockedBy.sort()) === JSON.stringify((defaultValues.blockedBy || []).sort()) && + formStage === defaultValues.stage && + JSON.stringify(formDescriptionBullets) === JSON.stringify(defaultValues.descriptionBullets) && + (leadId !== wbsElement.lead?.userId.toString() || managerId !== wbsElement.manager?.userId.toString()) + ); + }; + const onSubmit = async (data: WorkPackageFormViewPayload) => { const { name, startDate, duration, blockedBy, crId, stage, descriptionBullets } = data; const blockedByWbsNums = blockedBy.map((blocker) => validateWBS(blocker)); try { + const onlyLeadershipChanged = checkOnlyLeadershipChanged( + name, + startDate, + duration, + blockedBy, + stage, + descriptionBullets + ); + + if (onlyLeadershipChanged) { + const autoCRPayload = { + wbsNum: wbsElement.wbsNum, + workPackageId: defaultValues?.workPackageId, + leadId, + managerId + }; + // await createAutoApprovedLeadershipCR(autoCRPayload); + exitActiveMode(); + return; + } + const payload = { leadId, managerId, @@ -270,6 +315,18 @@ const WorkPackageFormView: React.FC = ({ const startDate = watch('startDate'); const duration = watch('duration'); + // Calculate for submit button status + const onlyLeadershipChanged = defaultValues + ? checkOnlyLeadershipChanged( + watch('name'), + watch('startDate'), + watch('duration'), + watch('blockedBy'), + watch('stage'), + watch('descriptionBullets') + ) + : false; + const calculatedEndDate = dayjs(startDate) .add(7 * duration, 'day') .toDate(); @@ -311,7 +368,7 @@ const WorkPackageFormView: React.FC = ({ setIsModalOpen(true)} sx={{ mx: 1 }} @@ -328,7 +385,7 @@ const WorkPackageFormView: React.FC = ({ variant="contained" type="submit" sx={{ mx: 1 }} - disabled={!changeRequestInputExists && !!defaultValues} + disabled={!changeRequestInputExists && !!defaultValues && !onlyLeadershipChanged} > Submit From 50865d7d14fd10ec1e4cdaa571be144cc3dd2b6a Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sat, 21 Feb 2026 20:02:02 -0500 Subject: [PATCH 3/9] #3907 backend progress --- .../change-requests.controllers.ts | 19 +++++++ .../change-requests.query-args.ts | 15 ++++- src/backend/src/prisma/schema.prisma | 16 ++++++ .../src/routes/change-requests.routes.ts | 11 ++++ .../change-requests.transformer.ts | 55 +++++++++++++------ src/shared/src/types/change-request-types.ts | 14 ++++- 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/src/backend/src/controllers/change-requests.controllers.ts b/src/backend/src/controllers/change-requests.controllers.ts index d14ed33544..6da3eea31a 100644 --- a/src/backend/src/controllers/change-requests.controllers.ts +++ b/src/backend/src/controllers/change-requests.controllers.ts @@ -140,6 +140,25 @@ export default class ChangeRequestsController { } } + static async createLeadershipChangeRequest(req: Request, res: Response, next: NextFunction) { + try { + const { wbsNum, leadId, managerId } = req.body; + + const cr = await ChangeRequestsService.createLeadershipChangeRequest( + req.currentUser, + wbsNum.carNumber, + wbsNum.projectNumber, + wbsNum.workPackageNumber, + leadId, + managerId, + req.organization + ); + res.status(200).json(cr); + } catch (error: unknown) { + next(error); + } +} + static async createStandardChangeRequest(req: Request, res: Response, next: NextFunction) { try { const { wbsNum, type, what, why, proposedSolutions, projectProposedChanges, workPackageProposedChanges } = req.body; 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 e5b8fcfdfa..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 @@ -39,7 +39,10 @@ export const getChangeRequestQueryArgs = (organizationId: string) => }, budgetChangeRequest: true, deletedBy: getUserQueryArgs(organizationId), - requestedReviewers: getUserQueryArgs(organizationId) + requestedReviewers: getUserQueryArgs(organizationId), + leadershipChangeRequest: { + include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) } + } } }); @@ -58,7 +61,10 @@ export const getManyChangeRequestQueryArgs = (organizationId: string) => }, budgetChangeRequest: true, deletedBy: getUserQueryArgs(organizationId), - requestedReviewers: getUserQueryArgs(organizationId) + requestedReviewers: getUserQueryArgs(organizationId), + leadershipChangeRequest: { + include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) } + } } }); @@ -101,6 +107,9 @@ export const getChangeRequestWithProjectAndWorkPackageQueryArgs = (organizationI }, budgetChangeRequest: true, deletedBy: getUserQueryArgs(organizationId), - requestedReviewers: getUserQueryArgs(organizationId) + requestedReviewers: getUserQueryArgs(organizationId), + leadershipChangeRequest: { + include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) } + } } }); diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 3d3ba18d35..2271ba3f24 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -15,6 +15,7 @@ generator client { enum CR_Type { ISSUE DEFINITION_CHANGE + LEADERSHIP OTHER STAGE_GATE ACTIVATION @@ -278,6 +279,8 @@ model User { deniedEvents Event[] @relation(name: "deniedEventAttendee") deletedDocuments Document[] @relation(name: "deletedDocuments") createdDocuments Document[] @relation(name: "documentsCreatedBy") + leadershipCrAsLead Leadership_CR[] @relation(name: "leadershipCrLead") + leadershipCrAsManager Leadership_CR[] @relation(name: "leadershipCrManager") } model Role { @@ -367,6 +370,7 @@ model Change_Request { stageGateChangeRequest Stage_Gate_CR? activationChangeRequest Activation_CR? budgetChangeRequest Budget_CR? + leadershipChangeRequest Leadership_CR? notificationSlackThreads Message_Info[] @@unique([identifier, organizationId], name: "uniqueChangeRequest") @@ -464,6 +468,18 @@ model Budget_CR { @@index([changeRequestId]) } +model Leadership_CR { + leadershipCrId String @id @default(uuid()) + changeRequestId String @unique + changeRequest Change_Request @relation(fields: [changeRequestId], references: [crId]) + leadId String? + lead User? @relation(name: "leadershipCrLead", fields: [leadId], references: [userId]) + managerId String? + manager User? @relation(name: "leadershipCrManager", fields: [managerId], references: [userId]) + + @@index([changeRequestId]) +} + model Change { changeId String @id @default(uuid()) changeRequestId String diff --git a/src/backend/src/routes/change-requests.routes.ts b/src/backend/src/routes/change-requests.routes.ts index 887e080b00..dd63454325 100644 --- a/src/backend/src/routes/change-requests.routes.ts +++ b/src/backend/src/routes/change-requests.routes.ts @@ -115,4 +115,15 @@ changeRequestsRouter.post( ChangeRequestsController.requestCRReview ); +changeRequestsRouter.post( + '/new/leadership', + intMinZero(body('wbsNum.carNumber')), + intMinZero(body('wbsNum.projectNumber')), + intMinZero(body('wbsNum.workPackageNumber')), + nonEmptyString(body('leadId')).optional(), + nonEmptyString(body('managerId')).optional(), + validateInputs, + ChangeRequestsController.createLeadershipChangeRequest +); + export default changeRequestsRouter; diff --git a/src/backend/src/transformers/change-requests.transformer.ts b/src/backend/src/transformers/change-requests.transformer.ts index ccd4120ce3..a8c8a7a988 100644 --- a/src/backend/src/transformers/change-requests.transformer.ts +++ b/src/backend/src/transformers/change-requests.transformer.ts @@ -9,7 +9,8 @@ import { WorkPackageProposedChanges, WorkPackageStage, isProjectWbs, - BudgetChangeRequest + BudgetChangeRequest, + LeadershipChangeRequest } from 'shared'; import { wbsNumOf } from '../utils/utils.js'; import { calculateChangeRequestStatus, convertCRScopeWhyType } from '../utils/change-requests.utils.js'; @@ -76,7 +77,13 @@ const workPackageProposedChangesTransformer = ( export const changeRequestManyTransformer = ( changeRequest: Prisma.Change_RequestGetPayload -): ChangeRequest | StandardChangeRequest | ActivationChangeRequest | StageGateChangeRequest | BudgetChangeRequest => { +): + | ChangeRequest + | StandardChangeRequest + | ActivationChangeRequest + | StageGateChangeRequest + | BudgetChangeRequest + | LeadershipChangeRequest => { const status = calculateChangeRequestStatus(changeRequest); return { @@ -108,13 +115,17 @@ export const changeRequestManyTransformer = ( proposedSolutions: undefined, originalProjectData: undefined, originalWorkPackageData: undefined, - // activation cr fields - lead: changeRequest.activationChangeRequest?.lead - ? userTransformer(changeRequest.activationChangeRequest.lead) - : undefined, - manager: changeRequest.activationChangeRequest?.manager - ? userTransformer(changeRequest.activationChangeRequest.manager) - : undefined, + // activation + leadership cr fields + lead: changeRequest.leadershipChangeRequest?.lead + ? userTransformer(changeRequest.leadershipChangeRequest.lead) + : changeRequest.activationChangeRequest?.lead + ? userTransformer(changeRequest.activationChangeRequest.lead) + : undefined, + manager: changeRequest.leadershipChangeRequest?.manager + ? userTransformer(changeRequest.leadershipChangeRequest.manager) + : changeRequest.activationChangeRequest?.manager + ? userTransformer(changeRequest.activationChangeRequest.manager) + : undefined, startDate: changeRequest.activationChangeRequest?.startDate ?? undefined, confirmDetails: changeRequest.activationChangeRequest?.confirmDetails ?? undefined, // stage gate cr fields @@ -128,7 +139,13 @@ export const changeRequestManyTransformer = ( const changeRequestTransformer = ( changeRequest: Prisma.Change_RequestGetPayload -): ChangeRequest | StandardChangeRequest | ActivationChangeRequest | StageGateChangeRequest | BudgetChangeRequest => { +): + | ChangeRequest + | StandardChangeRequest + | ActivationChangeRequest + | StageGateChangeRequest + | BudgetChangeRequest + | LeadershipChangeRequest => { const status = calculateChangeRequestStatus(changeRequest); const wbsName = changeRequest.wbsElement @@ -189,13 +206,17 @@ const changeRequestTransformer = ( originalWorkPackageData: changeRequest.scopeChangeRequest?.wbsOriginalData?.workPackageProposedChanges ? workPackageProposedChangesTransformer(changeRequest.scopeChangeRequest.wbsOriginalData.workPackageProposedChanges) : undefined, - // activation cr fields - lead: changeRequest.activationChangeRequest?.lead - ? userTransformer(changeRequest.activationChangeRequest.lead) - : undefined, - manager: changeRequest.activationChangeRequest?.manager - ? userTransformer(changeRequest.activationChangeRequest.manager) - : undefined, + // activation + leadership cr fields + lead: changeRequest.leadershipChangeRequest?.lead + ? userTransformer(changeRequest.leadershipChangeRequest.lead) + : changeRequest.activationChangeRequest?.lead + ? userTransformer(changeRequest.activationChangeRequest.lead) + : undefined, + manager: changeRequest.leadershipChangeRequest?.manager + ? userTransformer(changeRequest.leadershipChangeRequest.manager) + : changeRequest.activationChangeRequest?.manager + ? userTransformer(changeRequest.activationChangeRequest.manager) + : undefined, startDate: changeRequest.activationChangeRequest?.startDate ?? undefined, confirmDetails: changeRequest.activationChangeRequest?.confirmDetails ?? undefined, // stage gate cr fields diff --git a/src/shared/src/types/change-request-types.ts b/src/shared/src/types/change-request-types.ts index 0646ee41fe..5161662b1a 100644 --- a/src/shared/src/types/change-request-types.ts +++ b/src/shared/src/types/change-request-types.ts @@ -34,7 +34,8 @@ export const ChangeRequestType = { Other: 'OTHER', StageGate: 'STAGE_GATE', Activation: 'ACTIVATION', - Budget: 'BUDGET' + Budget: 'BUDGET', + Leadership: 'LEADERSHIP' } as const; // eslint-disable-next-line @typescript-eslint/no-redeclare export type ChangeRequestType = (typeof ChangeRequestType)[keyof typeof ChangeRequestType]; @@ -79,6 +80,11 @@ export interface BudgetChangeRequest extends ChangeRequest { proposedBudget: number; } +export interface LeadershipChangeRequest extends ChangeRequest { + lead?: User; + manager?: User; +} + export interface ChangeRequestExplanation { type: ChangeRequestReason; explain: string; @@ -155,3 +161,9 @@ export interface WorkPackageProposedChangesCreateArgs extends WBSProposedChanges stage?: WorkPackageStage; blockedBy: WbsNumber[]; } + +export interface LeadershipChangeCreateArgs extends WBSProposedChangesCreateArgs { + wbsNum: WbsNumber; + leadId?: string; + managerId?: string; +} \ No newline at end of file From ceb8b097dddf06d2920fdd3fd93de8f638b2f2e4 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sun, 22 Feb 2026 21:56:57 -0500 Subject: [PATCH 4/9] #3907 added endpoint + hook --- .../migration.sql | 27 ++++ .../src/services/change-requests.services.ts | 126 ++++++++++++++++++ src/frontend/src/apis/change-requests.api.ts | 23 ++++ .../src/hooks/change-requests.hooks.ts | 33 ++++- .../ProjectForm/ProjectEditContainer.tsx | 18 ++- .../WorkPackageForm/EditWorkPackageForm.tsx | 12 +- .../pages/WorkPackageForm/WorkPackageForm.tsx | 7 +- .../WorkPackageForm/WorkPackageFormView.tsx | 13 +- src/frontend/src/utils/urls.ts | 2 + src/shared/src/types/change-request-types.ts | 3 +- 10 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 src/backend/src/prisma/migrations/20260223001543_leadership_cr/migration.sql diff --git a/src/backend/src/prisma/migrations/20260223001543_leadership_cr/migration.sql b/src/backend/src/prisma/migrations/20260223001543_leadership_cr/migration.sql new file mode 100644 index 0000000000..e3eed0574b --- /dev/null +++ b/src/backend/src/prisma/migrations/20260223001543_leadership_cr/migration.sql @@ -0,0 +1,27 @@ +-- AlterEnum +ALTER TYPE "CR_Type" ADD VALUE 'LEADERSHIP'; + +-- CreateTable +CREATE TABLE "Leadership_CR" ( + "leadershipCrId" TEXT NOT NULL, + "changeRequestId" TEXT NOT NULL, + "leadId" TEXT, + "managerId" TEXT, + + CONSTRAINT "Leadership_CR_pkey" PRIMARY KEY ("leadershipCrId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Leadership_CR_changeRequestId_key" ON "Leadership_CR"("changeRequestId"); + +-- CreateIndex +CREATE INDEX "Leadership_CR_changeRequestId_idx" ON "Leadership_CR"("changeRequestId"); + +-- AddForeignKey +ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_changeRequestId_fkey" FOREIGN KEY ("changeRequestId") REFERENCES "Change_Request"("crId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 2a432ba641..bb2797cea6 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -1008,6 +1008,132 @@ export default class ChangeRequestsService { return changeRequestTransformer(createdChangeRequest); } + /** + * Validates and creates a leadership change request, auto-approved immediately. + * Updates the lead and/or manager of a project or work package without requiring review. + * @param submitter the user creating the cr + * @param carNumber the car number for the wbs element + * @param projectNumber the project number for the wbs element + * @param workPackageNumber the work package number for the wbs element + * @param leadId the id of the new lead + * @param managerId the id of the new manager + * @param organization the organization the user is currently in + * @returns the id of the created cr + */ + static async createLeadershipChangeRequest( + submitter: User, + carNumber: number, + projectNumber: number, + workPackageNumber: number, + leadId: string | undefined, + managerId: string | undefined, + organization: Organization + ): Promise { + if (await userHasPermission(submitter.userId, organization.organizationId, isGuest)) + throw new AccessDeniedGuestException('create leadership change requests'); + + // verify wbs element exists + const wbsElement = await prisma.wBS_Element.findUnique({ + where: { + wbsNumber: { + carNumber, + projectNumber, + workPackageNumber, + organizationId: organization.organizationId + } + } + }); + + if (!wbsElement) throw new NotFoundException('WBS Element', wbsPipe({ carNumber, projectNumber, workPackageNumber })); + if (wbsElement.dateDeleted) + throw new DeletedException('WBS Element', wbsPipe({ carNumber, projectNumber, workPackageNumber })); + if (wbsElement.organizationId !== organization.organizationId) throw new InvalidOrganizationException('WBS Element'); + + const numChangeRequests = await prisma.change_Request.count({ + where: { organizationId: organization.organizationId } + }); + + const createdCR = await prisma.change_Request.create({ + data: { + submitter: { connect: { userId: submitter.userId } }, + wbsElement: { connect: { wbsElementId: wbsElement.wbsElementId } }, + type: CR_Type.LEADERSHIP, + organization: { connect: { organizationId: organization.organizationId } }, + identifier: numChangeRequests + 1, + leadershipChangeRequest: { + create: { + ...(leadId && { lead: { connect: { userId: leadId } } }), + ...(managerId && { manager: { connect: { userId: managerId } } }) + } + } + } + }); + + await ChangeRequestsService.applyLeadershipChangeRequest(createdCR.crId, wbsElement, submitter, leadId, managerId); + + return createdCR.crId; + } + + /** + * Applies a leadership change request by updating the wbs element's lead/manager + * and auto-approving the change request. + */ + private static async applyLeadershipChangeRequest( + crId: string, + wbsElement: { wbsElementId: string; leadId: string | null; managerId: string | null }, + submitter: User, + leadId: string | undefined, + managerId: string | undefined + ): Promise { + await prisma.$transaction(async (tx) => { + await tx.change_Request.update({ + where: { crId }, + data: { + reviewer: { connect: { userId: submitter.userId } }, + dateReviewed: new Date(), + accepted: true, + reviewNotes: 'Auto-approved: leadership change only' + } + }); + + await tx.wBS_Element.update({ + where: { wbsElementId: wbsElement.wbsElementId }, + data: { + ...(leadId && { lead: { connect: { userId: leadId } } }), + ...(managerId && { manager: { connect: { userId: managerId } } }) + } + }); + + const changes: { changeRequestId: string; implementerId: string; wbsElementId: string; detail: string }[] = []; + + if (leadId !== undefined) { + const oldLead = await getUserFullName(wbsElement.leadId ?? null); + const newLead = await getUserFullName(leadId); + changes.push({ + changeRequestId: crId, + implementerId: submitter.userId, + wbsElementId: wbsElement.wbsElementId, + detail: buildChangeDetail('lead', oldLead, newLead) + }); + } + + if (managerId !== undefined) { + const oldManager = await getUserFullName(wbsElement.managerId ?? null); + const newManager = await getUserFullName(managerId); + changes.push({ + changeRequestId: crId, + implementerId: submitter.userId, + wbsElementId: wbsElement.wbsElementId, + detail: buildChangeDetail('manager', oldManager, newManager) + }); + } + + if (changes.length > 0) { + await tx.change.createMany({ data: changes }); + } + }); + } + /** * Validates and creates a standard change request * @param submitter The user creating the cr diff --git a/src/frontend/src/apis/change-requests.api.ts b/src/frontend/src/apis/change-requests.api.ts index e2f83fbc42..7f101a45de 100644 --- a/src/frontend/src/apis/change-requests.api.ts +++ b/src/frontend/src/apis/change-requests.api.ts @@ -154,6 +154,29 @@ export const createBudgetChangeRequest = ( }); }; +/** + * Create a leadership change request + * Updating the lead and/or manager of a project or work package does not require review + * @param submitterId The id of the user creating the change request + * @param wbsNum The WBS number of the project or work package being updated + * @param leadId The id of the new lead + * @param managerId The id of the new manager + */ +export const createLeadershipChangeRequest = ( + submitterId: string, + wbsNum: WbsNumber, + leadId?: string, + managerId?: string +) => { + return axios.post<{ message: string }>(apiUrls.changeRequestsCreateLeadership(), { + submitterId, + wbsNum, + leadId, + managerId, + type: ChangeRequestType.Leadership + }); +}; + /** * Create a propose solution * @param submitterId The ID of the user creating the change request. diff --git a/src/frontend/src/hooks/change-requests.hooks.ts b/src/frontend/src/hooks/change-requests.hooks.ts index e2e4cd0034..3d64858c68 100644 --- a/src/frontend/src/hooks/change-requests.hooks.ts +++ b/src/frontend/src/hooks/change-requests.hooks.ts @@ -11,7 +11,8 @@ import { ProjectProposedChangesCreateArgs, ProposedSolutionCreateArgs, WbsNumber, - WorkPackageProposedChangesCreateArgs + WorkPackageProposedChangesCreateArgs, + LeadershipChangeCreateArgs } from 'shared'; import { createActivationChangeRequest, @@ -26,7 +27,8 @@ import { getToReviewChangeRequests, getUnreviewedChangeRequests, getApprovedChangeRequests, - createBudgetChangeRequest + createBudgetChangeRequest, + createLeadershipChangeRequest } from '../apis/change-requests.api'; /** @@ -238,6 +240,33 @@ export const useCreateBudgetChangeRequest = () => { ); }; +/** + * Custome React hook to create a leadership change request + * to change lead and/or manager of a project or work package + */ +export const useCreateLeadershipChangeRequest = () => { + const queryClient = useQueryClient(); + return useMutation<{ message: string }, Error, LeadershipChangeCreateArgs>( + ['change-requests', 'create', 'leadership'], + async (payload: LeadershipChangeCreateArgs) => { + const { data } = await createLeadershipChangeRequest( + payload.submitterId, + payload.wbsNum, + payload.leadId, + payload.managerId + ); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['change-requests']); + queryClient.invalidateQueries(['projects']); + queryClient.invalidateQueries(['work-packages']); + } + } + ); +}; + /** * Custom React Hook to create a proposed solution */ diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx index 5227684db2..a66012a2cb 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectEditContainer.tsx @@ -16,12 +16,17 @@ import { useQuery } from '../../../hooks/utils.hooks'; import * as yup from 'yup'; import { StandardChangeRequestType } from '../../CreateChangeRequestPage/CreateChangeRequestView'; import { FormInput, FormInput as ChangeRequestFormInput } from '../../CreateChangeRequestPage/CreateChangeRequestView'; -import { CreateStandardChangeRequestPayload, useCreateStandardChangeRequest } from '../../../hooks/change-requests.hooks'; +import { + CreateStandardChangeRequestPayload, + useCreateLeadershipChangeRequest, + useCreateStandardChangeRequest +} from '../../../hooks/change-requests.hooks'; import { routes } from '../../../utils/routes'; import { useHistory } from 'react-router-dom'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import ProjectFormContainer from './ProjectForm'; +import { useCurrentUser } from '../../../hooks/users.hooks'; interface ProjectEditContainerProps { project: Project; @@ -34,6 +39,7 @@ const ProjectEditContainer: React.FC = ({ project, ex const toast = useToast(); const query = useQuery(); const history = useHistory(); + const user = useCurrentUser(); const { name, budget, summary, workPackages } = project; const [managerId, setManagerId] = useState(project.manager?.userId.toString()); const [leadId, setLeadId] = useState(project.lead?.userId.toString()); @@ -42,8 +48,8 @@ const ProjectEditContainer: React.FC = ({ project, ex const { mutateAsync, isLoading } = useEditSingleProject(project.wbsNum); const { mutateAsync: mutateCRAsync, isLoading: isCRHookLoading } = useCreateStandardChangeRequest(); - // TODO: Create auto-approved leadership CR hook - // const { mutateAsync: mutateAutoApprovedCR, isLoading: isAutoApprovedLoading } = useCreateAutoApprovedLeadershipCR(); + + const { mutateAsync: mutateLeadershipCR, isLoading: isLeadershipCRLoading } = useCreateLeadershipChangeRequest(); const { data: allLinkTypes, @@ -109,7 +115,7 @@ const ProjectEditContainer: React.FC = ({ project, ex } }); - if (isLoading || isCRHookLoading) return ; + if (isLoading || isCRHookLoading || isLeadershipCRLoading) return ; if (!allLinkTypes || allLinkTypesIsLoading) return ; if (allLinkTypesIsError) return ; @@ -225,12 +231,12 @@ const ProjectEditContainer: React.FC = ({ project, ex if (onlyLeadershipChanged) { const autoCRPayload = { + submitterId: user.userId, wbsNum: project.wbsNum, - projectId: project.id, leadId, managerId }; - // await mutateAutoApprovedCR(autoCRPayload); + await mutateLeadershipCR(autoCRPayload); exitEditMode(); return; } diff --git a/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx b/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx index db4acb5dc8..fe1c593bfc 100644 --- a/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx +++ b/src/frontend/src/pages/WorkPackageForm/EditWorkPackageForm.tsx @@ -4,7 +4,7 @@ import { useEditWorkPackage } from '../../hooks/work-packages.hooks'; import { useHistory } from 'react-router-dom'; import LoadingIndicator from '../../components/LoadingIndicator'; import * as yup from 'yup'; -import { useCreateStandardChangeRequest } from '../../hooks/change-requests.hooks'; +import { useCreateLeadershipChangeRequest, useCreateStandardChangeRequest } from '../../hooks/change-requests.hooks'; import { routes } from '../../utils/routes'; import { WorkPackageApiInputs } from '../../apis/work-packages.api'; @@ -20,12 +20,10 @@ const EditWorkPackageForm: React.FC = ({ wbsNum, workP const { mutateAsync: editWorkPackage, isLoading } = useEditWorkPackage(wbsNum); const { mutateAsync: createWorkPackageScopeCR, isLoading: createStandardChangeRequestIsLoading } = useCreateStandardChangeRequest(); - // TODO: Create auto-approved leadership CR hook for work packages - // const { mutateAsync: createAutoApprovedWPLeadershipCR, isLoading: createAutoApprovedWPIsLoading } = - // useCreateAutoApprovedWPLeadershipCR(); - if (isLoading || createStandardChangeRequestIsLoading) return ; - // if (isLoading || createStandardChangeRequestIsLoading || createAutoApprovedWPIsLoading) return ; + const { mutateAsync: mutateLeadershipCR, isLoading: isLeadershipCRLoading } = useCreateLeadershipChangeRequest(); + + if (isLoading || createStandardChangeRequestIsLoading || isLeadershipCRLoading) return ; const schema = yup.object().shape({ name: yup.string().required('Name is required!'), @@ -57,7 +55,7 @@ const EditWorkPackageForm: React.FC = ({ wbsNum, workP wbsNum={wbsNum} workPackageMutateAsync={editWorkPackageWrapper} createWorkPackageScopeCR={createWorkPackageScopeCR} - createAutoApprovedLeadershipCR={/*createAutoApprovedWPLeadershipCR*/ () => {}} + createLeadershipCR={mutateLeadershipCR} exitActiveMode={() => { setPageMode(false); history.push(`${history.location.pathname}`); diff --git a/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx b/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx index e633405050..da46cc3c9c 100644 --- a/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx +++ b/src/frontend/src/pages/WorkPackageForm/WorkPackageForm.tsx @@ -9,6 +9,7 @@ import { useSingleProject } from '../../hooks/projects.hooks'; import { WorkPackageApiInputs } from '../../apis/work-packages.api'; import { ObjectSchema } from 'yup'; import { CreateStandardChangeRequestPayload } from '../../hooks/change-requests.hooks'; +import { LeadershipChangeCreateArgs } from '../../../../shared'; interface WorkPackageFormProps { wbsNum: WbsNumber; @@ -16,7 +17,7 @@ interface WorkPackageFormProps { crId?: string; workPackageMutateAsync: (data: WorkPackageApiInputs) => void; createWorkPackageScopeCR: (data: CreateStandardChangeRequestPayload) => void; - createAutoApprovedLeadershipCR: (data: any) => void; // update this to auto approved cr payload + createLeadershipCR: (data: LeadershipChangeCreateArgs) => void; schema: ObjectSchema; breadcrumbs: { name: string; route: string }[]; } @@ -25,7 +26,7 @@ const WorkPackageForm: React.FC = ({ wbsNum, workPackageMutateAsync, createWorkPackageScopeCR, - createAutoApprovedLeadershipCR, + createLeadershipCR, exitActiveMode, crId, schema, @@ -78,7 +79,7 @@ const WorkPackageForm: React.FC = ({ exitActiveMode={exitActiveMode} workPackageMutateAsync={workPackageMutateAsync} createWorkPackageScopeCR={createWorkPackageScopeCR} - createAutoApprovedLeadershipCR={createAutoApprovedLeadershipCR} + createLeadershipCR={createLeadershipCR} defaultValues={defaultValues} wbsElement={wbsElement} leadOrManagerOptions={leadOrManagerOptions} diff --git a/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx b/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx index ffd96ff3b7..521950a885 100644 --- a/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx +++ b/src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx @@ -11,7 +11,9 @@ import { validateWBS, WbsElement, wbsPipe, - WorkPackageTemplate + WorkPackageTemplate, + WorkPackageStage, + LeadershipChangeCreateArgs } from 'shared'; import { Control, @@ -34,7 +36,6 @@ import { useToast } from '../../hooks/toasts.hooks'; import { useCurrentUser } from '../../hooks/users.hooks'; import PageBreadcrumbs from '../../layouts/PageTitle/PageBreadcrumbs'; import { WorkPackageApiInputs } from '../../apis/work-packages.api'; -import { WorkPackageStage } from 'shared'; import { ObjectSchema } from 'yup'; import { getMonday, transformDate } from '../../utils/datetime.utils'; import { CreateStandardChangeRequestPayload } from '../../hooks/change-requests.hooks'; @@ -68,7 +69,7 @@ interface WorkPackageFormViewProps { exitActiveMode: () => void; workPackageMutateAsync: (data: WorkPackageApiInputs) => void; createWorkPackageScopeCR: (data: CreateStandardChangeRequestPayload) => void; - createAutoApprovedLeadershipCR: (data: any) => void; + createLeadershipCR: (data: LeadershipChangeCreateArgs) => void; defaultValues?: WorkPackageFormViewPayload; wbsElement: WbsElement; leadOrManagerOptions: User[]; @@ -93,7 +94,7 @@ const WorkPackageFormView: React.FC = ({ exitActiveMode, workPackageMutateAsync, createWorkPackageScopeCR, - createAutoApprovedLeadershipCR, + createLeadershipCR, defaultValues, wbsElement, leadOrManagerOptions, @@ -262,12 +263,12 @@ const WorkPackageFormView: React.FC = ({ if (onlyLeadershipChanged) { const autoCRPayload = { + submitterId: user.userId, wbsNum: wbsElement.wbsNum, - workPackageId: defaultValues?.workPackageId, leadId, managerId }; - // await createAutoApprovedLeadershipCR(autoCRPayload); + await createLeadershipCR(autoCRPayload); exitActiveMode(); return; } diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 316e21837d..393a7ce87e 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -131,6 +131,7 @@ const changeRequestsCreate = () => `${changeRequests()}/new`; const changeRequestsCreateActivation = () => `${changeRequestsCreate()}/activation`; const changeRequestsCreateStageGate = () => `${changeRequestsCreate()}/stage-gate`; const changeRequestsCreateBudget = () => `${changeRequestsCreate()}/budget`; +const changeRequestsCreateLeadership = () => `${changeRequestsCreate()}/leadership`; const changeRequestsCreateStandard = () => `${changeRequestsCreate()}/standard`; const changeRequestCreateProposeSolution = () => `${changeRequestsCreate()}/proposed-solution`; const changeRequestRequestReviewer = (id: string) => changeRequestsById(id) + '/request-review'; @@ -580,6 +581,7 @@ export const apiUrls = { changeRequestsCreateActivation, changeRequestsCreateStageGate, changeRequestsCreateBudget, + changeRequestsCreateLeadership, changeRequestsCreateStandard, changeRequestCreateProposeSolution, changeRequestRequestReviewer, diff --git a/src/shared/src/types/change-request-types.ts b/src/shared/src/types/change-request-types.ts index 5161662b1a..250307bcd3 100644 --- a/src/shared/src/types/change-request-types.ts +++ b/src/shared/src/types/change-request-types.ts @@ -162,7 +162,8 @@ export interface WorkPackageProposedChangesCreateArgs extends WBSProposedChanges blockedBy: WbsNumber[]; } -export interface LeadershipChangeCreateArgs extends WBSProposedChangesCreateArgs { +export interface LeadershipChangeCreateArgs { + submitterId: string; wbsNum: WbsNumber; leadId?: string; managerId?: string; From 881fa0f890da2c3133ae33948ffd6efa489f6f3d Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sun, 22 Feb 2026 22:16:42 -0500 Subject: [PATCH 5/9] #3907 fixes and prettier --- .../change-requests.controllers.ts | 32 +++++++++---------- .../WorkPackageForm/CreateWorkPackageForm.tsx | 1 + src/frontend/src/utils/enum-pipes.ts | 2 ++ src/shared/src/types/change-request-types.ts | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/backend/src/controllers/change-requests.controllers.ts b/src/backend/src/controllers/change-requests.controllers.ts index 6da3eea31a..208937bac8 100644 --- a/src/backend/src/controllers/change-requests.controllers.ts +++ b/src/backend/src/controllers/change-requests.controllers.ts @@ -141,23 +141,23 @@ export default class ChangeRequestsController { } static async createLeadershipChangeRequest(req: Request, res: Response, next: NextFunction) { - try { - const { wbsNum, leadId, managerId } = req.body; - - const cr = await ChangeRequestsService.createLeadershipChangeRequest( - req.currentUser, - wbsNum.carNumber, - wbsNum.projectNumber, - wbsNum.workPackageNumber, - leadId, - managerId, - req.organization - ); - res.status(200).json(cr); - } catch (error: unknown) { - next(error); + try { + const { wbsNum, leadId, managerId } = req.body; + + const cr = await ChangeRequestsService.createLeadershipChangeRequest( + req.currentUser, + wbsNum.carNumber, + wbsNum.projectNumber, + wbsNum.workPackageNumber, + leadId, + managerId, + req.organization + ); + res.status(200).json(cr); + } catch (error: unknown) { + next(error); + } } -} static async createStandardChangeRequest(req: Request, res: Response, next: NextFunction) { try { diff --git a/src/frontend/src/pages/WorkPackageForm/CreateWorkPackageForm.tsx b/src/frontend/src/pages/WorkPackageForm/CreateWorkPackageForm.tsx index 235d16baaa..cb000befa4 100644 --- a/src/frontend/src/pages/WorkPackageForm/CreateWorkPackageForm.tsx +++ b/src/frontend/src/pages/WorkPackageForm/CreateWorkPackageForm.tsx @@ -63,6 +63,7 @@ const CreateWorkPackageForm: React.FC = () => { wbsNum={validateWBS(wbsNum)} workPackageMutateAsync={createWorkPackageWrapper} createWorkPackageScopeCR={createWorkPackageScopeCR} + createLeadershipCR={() => {}} // leadership changes can't happen on creation exitActiveMode={() => history.push(`${routes.PROJECTS}/${projectWbsPipe(validateWBS(wbsNum))}`)} crId={crId ?? undefined} schema={schema} diff --git a/src/frontend/src/utils/enum-pipes.ts b/src/frontend/src/utils/enum-pipes.ts index 57926da60c..201e66fd01 100644 --- a/src/frontend/src/utils/enum-pipes.ts +++ b/src/frontend/src/utils/enum-pipes.ts @@ -55,6 +55,8 @@ export const ChangeRequestTypeTextPipe: (type: ChangeRequestType) => string = (t return 'Other'; case ChangeRequestType.Budget: return 'Budget'; + case ChangeRequestType.Leadership: + return 'Leadership'; } }; diff --git a/src/shared/src/types/change-request-types.ts b/src/shared/src/types/change-request-types.ts index 250307bcd3..a57b56b354 100644 --- a/src/shared/src/types/change-request-types.ts +++ b/src/shared/src/types/change-request-types.ts @@ -167,4 +167,4 @@ export interface LeadershipChangeCreateArgs { wbsNum: WbsNumber; leadId?: string; managerId?: string; -} \ No newline at end of file +} From 5b03054cfe6ae08044b2cf5506181d9f6112dd4e Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sun, 22 Feb 2026 22:55:18 -0500 Subject: [PATCH 6/9] #3907 button status fix --- .../src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx index dfcfc9fd2b..e9a56dc052 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectForm/ProjectForm.tsx @@ -241,7 +241,7 @@ const ProjectFormContainer: React.FC = ({ variant="contained" onClick={() => setIsModalOpen(true)} sx={{ mx: 1 }} - disabled={changeRequestInputExists || onlyLeadershipChanged} + disabled={changeRequestInputExists || !onlyLeadershipChanged} > Create Change Request @@ -251,7 +251,7 @@ const ProjectFormContainer: React.FC = ({ Cancel Date: Sun, 22 Feb 2026 23:03:48 -0500 Subject: [PATCH 7/9] #3907 reload fix --- src/frontend/src/hooks/change-requests.hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/hooks/change-requests.hooks.ts b/src/frontend/src/hooks/change-requests.hooks.ts index 3d64858c68..72d0ec8ac3 100644 --- a/src/frontend/src/hooks/change-requests.hooks.ts +++ b/src/frontend/src/hooks/change-requests.hooks.ts @@ -247,7 +247,7 @@ export const useCreateBudgetChangeRequest = () => { export const useCreateLeadershipChangeRequest = () => { const queryClient = useQueryClient(); return useMutation<{ message: string }, Error, LeadershipChangeCreateArgs>( - ['change-requests', 'create', 'leadership'], + ['change requests', 'create', 'leadership'], async (payload: LeadershipChangeCreateArgs) => { const { data } = await createLeadershipChangeRequest( payload.submitterId, @@ -259,7 +259,7 @@ export const useCreateLeadershipChangeRequest = () => { }, { onSuccess: () => { - queryClient.invalidateQueries(['change-requests']); + queryClient.invalidateQueries(['change requests']); queryClient.invalidateQueries(['projects']); queryClient.invalidateQueries(['work-packages']); } From a0d2bc9f93aa728a03bdc5ca3d1401238643019e Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sun, 22 Feb 2026 23:12:06 -0500 Subject: [PATCH 8/9] #3907 query fix --- src/frontend/src/hooks/change-requests.hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/hooks/change-requests.hooks.ts b/src/frontend/src/hooks/change-requests.hooks.ts index 72d0ec8ac3..84061fac51 100644 --- a/src/frontend/src/hooks/change-requests.hooks.ts +++ b/src/frontend/src/hooks/change-requests.hooks.ts @@ -261,7 +261,7 @@ export const useCreateLeadershipChangeRequest = () => { onSuccess: () => { queryClient.invalidateQueries(['change requests']); queryClient.invalidateQueries(['projects']); - queryClient.invalidateQueries(['work-packages']); + queryClient.invalidateQueries(['work packages']); } } ); From 8c0e9cd18d40d4b9f3db1e648471055cb8b11c4c Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Mon, 23 Feb 2026 20:05:35 -0500 Subject: [PATCH 9/9] #3907 added cr check --- src/backend/src/services/change-requests.services.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index bb2797cea6..8797183640 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -1049,6 +1049,9 @@ export default class ChangeRequestsService { throw new DeletedException('WBS Element', wbsPipe({ carNumber, projectNumber, workPackageNumber })); if (wbsElement.organizationId !== organization.organizationId) throw new InvalidOrganizationException('WBS Element'); + // avoid merge conflicts + await validateNoUnreviewedOpenCRs(wbsElement.wbsElementId); + const numChangeRequests = await prisma.change_Request.count({ where: { organizationId: organization.organizationId } });