Skip to content

Commit 0fb0b4c

Browse files
authored
Merge pull request #3965 from Northeastern-Electric-Racing/#3907-automatic-change-requests-updating-roles
#3907 automatic change requests updating roles
2 parents df35084 + dc20af4 commit 0fb0b4c

20 files changed

Lines changed: 486 additions & 36 deletions

File tree

src/backend/src/controllers/change-requests.controllers.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ export default class ChangeRequestsController {
140140
}
141141
}
142142

143+
static async createLeadershipChangeRequest(req: Request, res: Response, next: NextFunction) {
144+
try {
145+
const { wbsNum, leadId, managerId } = req.body;
146+
147+
const cr = await ChangeRequestsService.createLeadershipChangeRequest(
148+
req.currentUser,
149+
wbsNum.carNumber,
150+
wbsNum.projectNumber,
151+
wbsNum.workPackageNumber,
152+
leadId,
153+
managerId,
154+
req.organization
155+
);
156+
res.status(200).json(cr);
157+
} catch (error: unknown) {
158+
next(error);
159+
}
160+
}
161+
143162
static async createStandardChangeRequest(req: Request, res: Response, next: NextFunction) {
144163
try {
145164
const { wbsNum, type, what, why, proposedSolutions, projectProposedChanges, workPackageProposedChanges } = req.body;

src/backend/src/prisma-query-args/change-requests.query-args.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export const getChangeRequestQueryArgs = (organizationId: string) =>
3939
},
4040
budgetChangeRequest: true,
4141
deletedBy: getUserQueryArgs(organizationId),
42-
requestedReviewers: getUserQueryArgs(organizationId)
42+
requestedReviewers: getUserQueryArgs(organizationId),
43+
leadershipChangeRequest: {
44+
include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) }
45+
}
4346
}
4447
});
4548

@@ -58,7 +61,10 @@ export const getManyChangeRequestQueryArgs = (organizationId: string) =>
5861
},
5962
budgetChangeRequest: true,
6063
deletedBy: getUserQueryArgs(organizationId),
61-
requestedReviewers: getUserQueryArgs(organizationId)
64+
requestedReviewers: getUserQueryArgs(organizationId),
65+
leadershipChangeRequest: {
66+
include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) }
67+
}
6268
}
6369
});
6470

@@ -101,6 +107,9 @@ export const getChangeRequestWithProjectAndWorkPackageQueryArgs = (organizationI
101107
},
102108
budgetChangeRequest: true,
103109
deletedBy: getUserQueryArgs(organizationId),
104-
requestedReviewers: getUserQueryArgs(organizationId)
110+
requestedReviewers: getUserQueryArgs(organizationId),
111+
leadershipChangeRequest: {
112+
include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) }
113+
}
105114
}
106115
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- AlterEnum
2+
ALTER TYPE "CR_Type" ADD VALUE 'LEADERSHIP';
3+
4+
-- CreateTable
5+
CREATE TABLE "Leadership_CR" (
6+
"leadershipCrId" TEXT NOT NULL,
7+
"changeRequestId" TEXT NOT NULL,
8+
"leadId" TEXT,
9+
"managerId" TEXT,
10+
11+
CONSTRAINT "Leadership_CR_pkey" PRIMARY KEY ("leadershipCrId")
12+
);
13+
14+
-- CreateIndex
15+
CREATE UNIQUE INDEX "Leadership_CR_changeRequestId_key" ON "Leadership_CR"("changeRequestId");
16+
17+
-- CreateIndex
18+
CREATE INDEX "Leadership_CR_changeRequestId_idx" ON "Leadership_CR"("changeRequestId");
19+
20+
-- AddForeignKey
21+
ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_changeRequestId_fkey" FOREIGN KEY ("changeRequestId") REFERENCES "Change_Request"("crId") ON DELETE RESTRICT ON UPDATE CASCADE;
22+
23+
-- AddForeignKey
24+
ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE;
25+
26+
-- AddForeignKey
27+
ALTER TABLE "Leadership_CR" ADD CONSTRAINT "Leadership_CR_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE;

src/backend/src/prisma/schema.prisma

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ generator client {
1515
enum CR_Type {
1616
ISSUE
1717
DEFINITION_CHANGE
18+
LEADERSHIP
1819
OTHER
1920
STAGE_GATE
2021
ACTIVATION
@@ -299,6 +300,8 @@ model User {
299300
deniedEvents Event[] @relation(name: "deniedEventAttendee")
300301
deletedDocuments Document[] @relation(name: "deletedDocuments")
301302
createdDocuments Document[] @relation(name: "documentsCreatedBy")
303+
leadershipCrAsLead Leadership_CR[] @relation(name: "leadershipCrLead")
304+
leadershipCrAsManager Leadership_CR[] @relation(name: "leadershipCrManager")
302305
prospectiveSponsorsContacted Prospective_Sponsor[] @relation(name: "prospectiveSponsorContactor")
303306
}
304307

@@ -389,6 +392,7 @@ model Change_Request {
389392
stageGateChangeRequest Stage_Gate_CR?
390393
activationChangeRequest Activation_CR?
391394
budgetChangeRequest Budget_CR?
395+
leadershipChangeRequest Leadership_CR?
392396
notificationSlackThreads Message_Info[]
393397
394398
@@unique([identifier, organizationId], name: "uniqueChangeRequest")
@@ -486,6 +490,18 @@ model Budget_CR {
486490
@@index([changeRequestId])
487491
}
488492

493+
model Leadership_CR {
494+
leadershipCrId String @id @default(uuid())
495+
changeRequestId String @unique
496+
changeRequest Change_Request @relation(fields: [changeRequestId], references: [crId])
497+
leadId String?
498+
lead User? @relation(name: "leadershipCrLead", fields: [leadId], references: [userId])
499+
managerId String?
500+
manager User? @relation(name: "leadershipCrManager", fields: [managerId], references: [userId])
501+
502+
@@index([changeRequestId])
503+
}
504+
489505
model Change {
490506
changeId String @id @default(uuid())
491507
changeRequestId String

src/backend/src/routes/change-requests.routes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,15 @@ changeRequestsRouter.post(
116116
ChangeRequestsController.requestCRReview
117117
);
118118

119+
changeRequestsRouter.post(
120+
'/new/leadership',
121+
intMinZero(body('wbsNum.carNumber')),
122+
intMinZero(body('wbsNum.projectNumber')),
123+
intMinZero(body('wbsNum.workPackageNumber')),
124+
nonEmptyString(body('leadId')).optional(),
125+
nonEmptyString(body('managerId')).optional(),
126+
validateInputs,
127+
ChangeRequestsController.createLeadershipChangeRequest
128+
);
129+
119130
export default changeRequestsRouter;

src/backend/src/services/change-requests.services.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,147 @@ export default class ChangeRequestsService {
10081008
return changeRequestTransformer(createdChangeRequest);
10091009
}
10101010

1011+
/**
1012+
* Validates and creates a leadership change request, auto-approved immediately.
1013+
* Updates the lead and/or manager of a project or work package without requiring review.
1014+
* @param submitter the user creating the cr
1015+
* @param carNumber the car number for the wbs element
1016+
* @param projectNumber the project number for the wbs element
1017+
* @param workPackageNumber the work package number for the wbs element
1018+
* @param leadId the id of the new lead
1019+
* @param managerId the id of the new manager
1020+
* @param organization the organization the user is currently in
1021+
* @returns the id of the created cr
1022+
*/
1023+
static async createLeadershipChangeRequest(
1024+
submitter: User,
1025+
carNumber: number,
1026+
projectNumber: number,
1027+
workPackageNumber: number,
1028+
leadId: string | undefined,
1029+
managerId: string | undefined,
1030+
organization: Organization
1031+
): Promise<string> {
1032+
if (await userHasPermission(submitter.userId, organization.organizationId, isGuest))
1033+
throw new AccessDeniedGuestException('create leadership change requests');
1034+
1035+
// verify wbs element exists
1036+
const wbsElement = await prisma.wBS_Element.findUnique({
1037+
where: {
1038+
wbsNumber: {
1039+
carNumber,
1040+
projectNumber,
1041+
workPackageNumber,
1042+
organizationId: organization.organizationId
1043+
}
1044+
},
1045+
select: {
1046+
wbsElementId: true,
1047+
dateDeleted: true,
1048+
organizationId: true,
1049+
leadId: true,
1050+
managerId: true
1051+
}
1052+
});
1053+
1054+
if (!wbsElement) throw new NotFoundException('WBS Element', wbsPipe({ carNumber, projectNumber, workPackageNumber }));
1055+
if (wbsElement.dateDeleted)
1056+
throw new DeletedException('WBS Element', wbsPipe({ carNumber, projectNumber, workPackageNumber }));
1057+
if (wbsElement.organizationId !== organization.organizationId) throw new InvalidOrganizationException('WBS Element');
1058+
1059+
// avoid merge conflicts
1060+
await validateNoUnreviewedOpenCRs(wbsElement.wbsElementId);
1061+
1062+
const numChangeRequests = await prisma.change_Request.count({
1063+
where: { organizationId: organization.organizationId }
1064+
});
1065+
1066+
const createdCR = await prisma.change_Request.create({
1067+
data: {
1068+
submitter: { connect: { userId: submitter.userId } },
1069+
wbsElement: { connect: { wbsElementId: wbsElement.wbsElementId } },
1070+
type: CR_Type.LEADERSHIP,
1071+
organization: { connect: { organizationId: organization.organizationId } },
1072+
identifier: numChangeRequests + 1,
1073+
leadershipChangeRequest: {
1074+
create: {
1075+
lead: leadId ? { connect: { userId: leadId } } : undefined,
1076+
manager: managerId ? { connect: { userId: managerId } } : undefined
1077+
}
1078+
}
1079+
}
1080+
});
1081+
1082+
await ChangeRequestsService.applyLeadershipChangeRequest(createdCR.crId, wbsElement, submitter, leadId, managerId);
1083+
1084+
return createdCR.crId;
1085+
}
1086+
1087+
/**
1088+
* Applies a leadership change request by updating the wbs element's lead/manager
1089+
* and auto-approving the change request.
1090+
*/
1091+
private static async applyLeadershipChangeRequest(
1092+
crId: string,
1093+
wbsElement: { wbsElementId: string; leadId: string | null; managerId: string | null },
1094+
submitter: User,
1095+
leadId: string | undefined,
1096+
managerId: string | undefined
1097+
): Promise<void> {
1098+
await prisma.$transaction(async (tx) => {
1099+
await tx.change_Request.update({
1100+
where: { crId },
1101+
data: {
1102+
reviewer: { connect: { userId: submitter.userId } },
1103+
dateReviewed: new Date(),
1104+
accepted: true,
1105+
reviewNotes: 'Auto-approved: leadership change only'
1106+
}
1107+
});
1108+
1109+
await tx.wBS_Element.update({
1110+
where: { wbsElementId: wbsElement.wbsElementId },
1111+
data: {
1112+
lead: leadId ? { connect: { userId: leadId } } : { disconnect: true },
1113+
manager: managerId ? { connect: { userId: managerId } } : { disconnect: true }
1114+
}
1115+
});
1116+
1117+
const changes: { changeRequestId: string; implementerId: string; wbsElementId: string; detail: string }[] = [];
1118+
1119+
const oldLeadId = wbsElement.leadId ?? undefined;
1120+
const oldManagerId = wbsElement.managerId ?? undefined;
1121+
1122+
if (leadId !== oldLeadId) {
1123+
// only update if lead changed
1124+
const oldLead = await getUserFullName(wbsElement.leadId ?? null);
1125+
const newLead = await getUserFullName(leadId ?? null);
1126+
changes.push({
1127+
changeRequestId: crId,
1128+
implementerId: submitter.userId,
1129+
wbsElementId: wbsElement.wbsElementId,
1130+
detail: buildChangeDetail('lead', oldLead, newLead)
1131+
});
1132+
}
1133+
1134+
if (managerId !== oldManagerId) {
1135+
// only update if manager changed
1136+
const oldManager = await getUserFullName(wbsElement.managerId ?? null);
1137+
const newManager = await getUserFullName(managerId ?? null);
1138+
changes.push({
1139+
changeRequestId: crId,
1140+
implementerId: submitter.userId,
1141+
wbsElementId: wbsElement.wbsElementId,
1142+
detail: buildChangeDetail('manager', oldManager, newManager)
1143+
});
1144+
}
1145+
1146+
if (changes.length > 0) {
1147+
await tx.change.createMany({ data: changes });
1148+
}
1149+
});
1150+
}
1151+
10111152
/**
10121153
* Validates and creates a standard change request
10131154
* @param submitter The user creating the cr

src/backend/src/transformers/change-requests.transformer.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
WorkPackageProposedChanges,
1010
WorkPackageStage,
1111
BudgetChangeRequest,
12-
isWorkPackageWbs
12+
isWorkPackageWbs,
13+
LeadershipChangeRequest
1314
} from 'shared';
1415
import { wbsNumOf } from '../utils/utils.js';
1516
import { calculateChangeRequestStatus, convertCRScopeWhyType } from '../utils/change-requests.utils.js';
@@ -76,7 +77,13 @@ const workPackageProposedChangesTransformer = (
7677

7778
export const changeRequestManyTransformer = (
7879
changeRequest: Prisma.Change_RequestGetPayload<ChangeRequestManyQueryArgs>
79-
): ChangeRequest | StandardChangeRequest | ActivationChangeRequest | StageGateChangeRequest | BudgetChangeRequest => {
80+
):
81+
| ChangeRequest
82+
| StandardChangeRequest
83+
| ActivationChangeRequest
84+
| StageGateChangeRequest
85+
| BudgetChangeRequest
86+
| LeadershipChangeRequest => {
8087
const status = calculateChangeRequestStatus(changeRequest);
8188

8289
return {
@@ -108,13 +115,17 @@ export const changeRequestManyTransformer = (
108115
proposedSolutions: undefined,
109116
originalProjectData: undefined,
110117
originalWorkPackageData: undefined,
111-
// activation cr fields
112-
lead: changeRequest.activationChangeRequest?.lead
113-
? userTransformer(changeRequest.activationChangeRequest.lead)
114-
: undefined,
115-
manager: changeRequest.activationChangeRequest?.manager
116-
? userTransformer(changeRequest.activationChangeRequest.manager)
117-
: undefined,
118+
// activation + leadership cr fields
119+
lead: changeRequest.leadershipChangeRequest?.lead
120+
? userTransformer(changeRequest.leadershipChangeRequest.lead)
121+
: changeRequest.activationChangeRequest?.lead
122+
? userTransformer(changeRequest.activationChangeRequest.lead)
123+
: undefined,
124+
manager: changeRequest.leadershipChangeRequest?.manager
125+
? userTransformer(changeRequest.leadershipChangeRequest.manager)
126+
: changeRequest.activationChangeRequest?.manager
127+
? userTransformer(changeRequest.activationChangeRequest.manager)
128+
: undefined,
118129
startDate: changeRequest.activationChangeRequest?.startDate ?? undefined,
119130
confirmDetails: changeRequest.activationChangeRequest?.confirmDetails ?? undefined,
120131
// stage gate cr fields
@@ -128,7 +139,13 @@ export const changeRequestManyTransformer = (
128139

129140
const changeRequestTransformer = (
130141
changeRequest: Prisma.Change_RequestGetPayload<ChangeRequestWithProjectAndWorkPackageQueryArgs>
131-
): ChangeRequest | StandardChangeRequest | ActivationChangeRequest | StageGateChangeRequest | BudgetChangeRequest => {
142+
):
143+
| ChangeRequest
144+
| StandardChangeRequest
145+
| ActivationChangeRequest
146+
| StageGateChangeRequest
147+
| BudgetChangeRequest
148+
| LeadershipChangeRequest => {
132149
const status = calculateChangeRequestStatus(changeRequest);
133150

134151
const wbsName = changeRequest.wbsElement
@@ -189,13 +206,17 @@ const changeRequestTransformer = (
189206
originalWorkPackageData: changeRequest.scopeChangeRequest?.wbsOriginalData?.workPackageProposedChanges
190207
? workPackageProposedChangesTransformer(changeRequest.scopeChangeRequest.wbsOriginalData.workPackageProposedChanges)
191208
: undefined,
192-
// activation cr fields
193-
lead: changeRequest.activationChangeRequest?.lead
194-
? userTransformer(changeRequest.activationChangeRequest.lead)
195-
: undefined,
196-
manager: changeRequest.activationChangeRequest?.manager
197-
? userTransformer(changeRequest.activationChangeRequest.manager)
198-
: undefined,
209+
// activation + leadership cr fields
210+
lead: changeRequest.leadershipChangeRequest?.lead
211+
? userTransformer(changeRequest.leadershipChangeRequest.lead)
212+
: changeRequest.activationChangeRequest?.lead
213+
? userTransformer(changeRequest.activationChangeRequest.lead)
214+
: undefined,
215+
manager: changeRequest.leadershipChangeRequest?.manager
216+
? userTransformer(changeRequest.leadershipChangeRequest.manager)
217+
: changeRequest.activationChangeRequest?.manager
218+
? userTransformer(changeRequest.activationChangeRequest.manager)
219+
: undefined,
199220
startDate: changeRequest.activationChangeRequest?.startDate ?? undefined,
200221
confirmDetails: changeRequest.activationChangeRequest?.confirmDetails ?? undefined,
201222
// stage gate cr fields

0 commit comments

Comments
 (0)