Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
825629c
#3867 first round of changes
wavehassman Jan 15, 2026
669a815
bom feature branch
wavehassman Jan 15, 2026
ba79f6e
#3867 fix controller
wavehassman Jan 15, 2026
dbe0dfa
#3867 migration changes
wavehassman Jan 16, 2026
51b5ab4
Merge pull request #3878 from Northeastern-Electric-Racing/#3867-sche…
wavehassman Jan 19, 2026
bc51629
#3883 added button
rbessin Jan 21, 2026
6324fb4
#3883 fixed button disable rather than hide when user doesn't have re…
rbessin Jan 21, 2026
b65611b
#3879: Changed layout and validation of the material form.
nunnyu Jan 22, 2026
c5aee7e
#3879: Moved RR# to additional info & fixed bug with quantity falsey …
nunnyu Jan 22, 2026
a2b3ad2
#3879: Consistency with tooltips and improved visual clarity
nunnyu Jan 22, 2026
4e47b0f
#3879: prettier linting fixes
nunnyu Jan 22, 2026
3ae89b1
#3883 changed Copy Existing BOM button to use isGuest
rbessin Jan 22, 2026
965fb7c
Merge pull request #3910 from Northeastern-Electric-Racing/#3883-Copy…
wavehassman Jan 22, 2026
baad1cf
#3880: Added Copy From Existing BOM Button
Jan 22, 2026
9994ada
prettier checks
Jan 22, 2026
3148057
TypeScript Check fix
Jan 22, 2026
64b76f5
#3886 added functionality
rbessin Jan 25, 2026
6589127
undid modal changes
Jan 25, 2026
fda946d
only in create form
Jan 25, 2026
e9eef79
Merge pull request #3915 from Northeastern-Electric-Racing/#3880-add-…
wavehassman Jan 25, 2026
bf00b51
#3879: setting parameter orders back to what they were to fix test fa…
nunnyu Jan 28, 2026
db45c94
fixed validation and enum issues
rbessin Jan 29, 2026
1179d95
Merge pull request #3912 from Northeastern-Electric-Racing/#3879-Reor…
wavehassman Jan 29, 2026
061a3cc
removed assembly copying
rbessin Jan 30, 2026
4d389c7
fixed linting error by removing assembly map
rbessin Jan 30, 2026
aadc681
#3891 accidentally completed two tickets
wavehassman Feb 1, 2026
a201f84
#3891 prettier check
wavehassman Feb 1, 2026
ca12916
updated validation, id handling and added tests
rbessin Feb 4, 2026
b3d6956
fixed linting
rbessin Feb 4, 2026
cb71d5f
#3891 requested change
wavehassman Feb 5, 2026
af64459
#3891 fix tests
wavehassman Feb 5, 2026
b3ce3cb
Merge pull request #3946 from Northeastern-Electric-Racing/#3891-upda…
wavehassman Feb 5, 2026
a1cbdd0
made optimizations
rbessin Feb 6, 2026
d213c6a
moved validation + removed redundant id checks
rbessin Feb 6, 2026
2354b31
removed unnecessary import
rbessin Feb 6, 2026
981e56a
Merge branch 'develop' into feature/bom-improvements
wavehassman Feb 7, 2026
8a6b34a
#3887: added dropdown for materials, and prints the material on select.
nunnyu Feb 9, 2026
641fd9b
#3887: selected material shows up when chosen on submit and in UI
nunnyu Feb 9, 2026
65255bd
#3888 added create material button for rr form
rbessin Feb 16, 2026
9442e50
#3888 fixed linting errors
rbessin Feb 16, 2026
3714255
#3887: wrapped autocomplete in form
nunnyu Feb 17, 2026
cd25f2f
fixed type issues
rbessin Feb 18, 2026
0889f90
#3887: changed label to include part number
nunnyu Feb 19, 2026
eb517e7
#3893: added create RR button for materials wout linked RR
Feb 20, 2026
567eb1f
linting/prettier checks
Feb 20, 2026
734d767
linting issues
Feb 20, 2026
bcd9b18
fixed math error
Feb 20, 2026
9f2fea1
#3977 updated form to preserve dropdown display
rbessin Feb 20, 2026
5a4f4d9
#3887 updated form to preserve dropdown display
rbessin Feb 20, 2026
275e638
Merge branch '#3887-Material-Autocomplete' of github.com:Northeastern…
rbessin Feb 20, 2026
5e6b12a
final fix linting
rbessin Feb 20, 2026
ed869ad
Merge pull request #3922 from Northeastern-Electric-Racing/#3886-Impl…
chpy04 Feb 20, 2026
5569e59
#3888 fixed variable naming
rbessin Feb 21, 2026
fb1e1bf
#3888 fixed prettier error
rbessin Feb 21, 2026
d37c9cb
Merge pull request #3984 from Northeastern-Electric-Racing/#3893-add-…
chpy04 Feb 21, 2026
b346137
Merge pull request #3976 from Northeastern-Electric-Racing/#3888-Crea…
chpy04 Feb 21, 2026
27aa039
#3887 resolved comments
rbessin Feb 22, 2026
50ea47a
#3887 resolved merge conflicts
rbessin Feb 22, 2026
d3adccd
readded create material option and fixed linting error
rbessin Feb 22, 2026
665febd
#3887 fixed materialId not being included in WBS reimbursement produc…
rbessin Feb 24, 2026
bf62b6f
#3887 fixed prettier issue
rbessin Feb 24, 2026
b4aef2c
#3887 updated code to handle string-based and material-based products
rbessin Feb 25, 2026
d4e470f
#3887 fixed multi-line prettier error
rbessin Feb 25, 2026
d87db92
Merge pull request #3977 from Northeastern-Electric-Racing/#3887-Mate…
wavehassman Feb 25, 2026
4f1e189
fixed merge conflicts + comment out buttons
wavehassman Feb 26, 2026
e795e1e
update yarn.lock
wavehassman Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/backend/src/controllers/projects.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,17 @@ export default class ProjectsController {
name,
status,
materialTypeName,
linkUrl,
wbsNum,
req.organization,
manufacturerName,
manufacturerPartNumber,
quantity,
price,
subtotal,
linkUrl,
wbsNum,
req.organization,
notes,
assemblyId,
pdmFileName === '' ? undefined : pdmFileName,
pdmFileName,
unitName,
reimbursementRequestId
);
Expand All @@ -236,6 +236,22 @@ export default class ProjectsController {
}
}

static async copyMaterialsToProject(req: Request, res: Response, next: NextFunction) {
try {
const { materialIds, destinationWbsNum } = req.body;

const newMaterialIds = await BillOfMaterialsService.copyMaterialsToProject(
req.currentUser,
materialIds,
destinationWbsNum,
req.organization
);
res.status(200).json(newMaterialIds);
} catch (error: unknown) {
next(error);
}
}

static async createManufacturer(req: Request, res: Response, next: NextFunction) {
try {
const { name } = req.body;
Expand Down Expand Up @@ -379,17 +395,17 @@ export default class ProjectsController {
name,
status,
materialTypeName,
linkUrl,
req.organization,
manufacturerName,
manufacturerPartNumber,
quantity,
price,
subtotal,
linkUrl,
req.organization,
notes,
unitName,
assemblyId,
pdmFileName === '' ? undefined : pdmFileName,
pdmFileName,
reimbursementRequestId
);
res.status(200).json(updatedMaterial);
Expand Down
2 changes: 2 additions & 0 deletions src/backend/src/prisma-query-args/bom.query-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const getMaterialQueryArgs = (organizationId: string) =>
materialType: true,
unit: true,
manufacturer: true,
reimbursementProducts: false,
reimbursementRequest: getReimbursementRequestQueryArgs(organizationId)
}
});
Expand All @@ -37,6 +38,7 @@ export const getMaterialPreviewQueryArgs = (organizationId: string) =>
unit: true,
manufacturer: true,
materialType: true,
reimbursementProducts: false,
reimbursementRequest: getReimbursementRequestQueryArgs(organizationId)
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const getReimbursementProductQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.Reimbursement_ProductDefaultArgs>()({
include: {
refundSources: getRefundSourceQueryArgs(organizationId),
reimbursementProductReason: getReimbursementProductReasonQueryArgs(organizationId)
reimbursementProductReason: getReimbursementProductReasonQueryArgs(organizationId),
material: true
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
-- DropForeignKey
ALTER TABLE "Material" DROP CONSTRAINT "Material_manufacturerId_fkey";

-- AlterTable
ALTER TABLE "Material" ALTER COLUMN "manufacturerId" DROP NOT NULL,
ALTER COLUMN "manufacturerPartNumber" DROP NOT NULL,
ALTER COLUMN "quantity" DROP NOT NULL,
ALTER COLUMN "price" DROP NOT NULL,
ALTER COLUMN "subtotal" DROP NOT NULL;

-- AlterTable
ALTER TABLE "Reimbursement_Product" ALTER COLUMN "name" DROP NOT NULL;

-- AlterTable
ALTER TABLE "Reimbursement_Product" ADD COLUMN "materialId" TEXT;

-- CreateIndex
CREATE INDEX "Reimbursement_Product_materialId_idx" ON "Reimbursement_Product"("materialId");

-- AddForeignKey
ALTER TABLE "Reimbursement_Product" ADD CONSTRAINT "Reimbursement_Product_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "Material"("materialId") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Material" ADD CONSTRAINT "Material_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer"("id") ON DELETE SET NULL ON UPDATE CASCADE;

DO $$
DECLARE
material_record RECORD;
new_reason_id TEXT;
BEGIN
-- Loop through all materials that are linked to RRs but don't have products yet
FOR material_record IN
SELECT
m."materialId",
m."name",
m."subtotal",
m."wbsElementId",
m."reimbursementRequestId"
FROM "Material" m
WHERE m."reimbursementRequestId" IS NOT NULL
AND m."dateDeleted" IS NULL
AND EXISTS (
-- Only migrate if the RR isn't deleted
SELECT 1 FROM "Reimbursement_Request" rr
WHERE rr."reimbursementRequestId" = m."reimbursementRequestId"
AND rr."dateDeleted" IS NULL
)
AND EXISTS (
-- Only migrate if the WBS element isn't deleted
SELECT 1 FROM "WBS_Element" wbs
WHERE wbs."wbsElementId" = m."wbsElementId"
AND wbs."dateDeleted" IS NULL
)
AND NOT EXISTS (
-- Skip if a product already links to this material for this RR
SELECT 1 FROM "Reimbursement_Product" rp
WHERE rp."materialId" = m."materialId"
AND rp."reimbursementRequestId" = m."reimbursementRequestId"
)
LOOP
-- Create a new reason for this material (can't reuse due to @unique and one to one relation)
INSERT INTO "Reimbursement_Product_Reason" (
"reimbursementProductReasonId",
"wbsElementId"
) VALUES (
gen_random_uuid(),
material_record."wbsElementId"
)
RETURNING "reimbursementProductReasonId" INTO new_reason_id;

-- Create the product linked to the material
INSERT INTO "Reimbursement_Product" (
"reimbursementProductId",
"name",
"cost",
"materialId",
"reimbursementProductReasonId",
"reimbursementRequestId"
) VALUES (
gen_random_uuid(),
material_record."name",
COALESCE(material_record."subtotal", 0),
material_record."materialId",
new_reason_id,
material_record."reimbursementRequestId"
);
END LOOP;
END $$;
34 changes: 19 additions & 15 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -745,9 +745,11 @@ model Reimbursement_Product_Reason {

model Reimbursement_Product {
reimbursementProductId String @id @default(uuid())
name String
name String?
dateDeleted DateTime?
cost Int
material Material? @relation(fields: [materialId], references: [materialId])
materialId String?
reimbursementProductReasonId String @unique
reimbursementProductReason Reimbursement_Product_Reason @relation(fields: [reimbursementProductReasonId], references: [reimbursementProductReasonId])
reimbursementRequestId String
Expand All @@ -756,6 +758,7 @@ model Reimbursement_Product {

@@index([reimbursementRequestId])
@@index([reimbursementProductReasonId])
@@index([materialId])
}

model Refund_Source {
Expand Down Expand Up @@ -907,34 +910,35 @@ model Assembly {
}

model Material {
materialId String @id @default(uuid())
assembly Assembly? @relation(fields: [assemblyId], references: [assemblyId])
materialId String @id @default(uuid())
assembly Assembly? @relation(fields: [assemblyId], references: [assemblyId])
assemblyId String?
name String
wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId])
wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId])
wbsElementId String
dateDeleted DateTime?
userDeleted User? @relation(fields: [userDeletedId], references: [userId], name: "materialDeleter")
userDeleted User? @relation(fields: [userDeletedId], references: [userId], name: "materialDeleter")
userDeletedId String?
dateCreated DateTime
userCreated User @relation(fields: [userCreatedId], references: [userId], name: "materialCreator")
userCreated User @relation(fields: [userCreatedId], references: [userId], name: "materialCreator")
userCreatedId String
status Material_Status
materialType Material_Type @relation(fields: [materialTypeId], references: [id])
materialType Material_Type @relation(fields: [materialTypeId], references: [id])
materialTypeId String
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id])
manufacturerId String
manufacturerPartNumber String
manufacturer Manufacturer? @relation(fields: [manufacturerId], references: [id])
manufacturerId String?
manufacturerPartNumber String?
pdmFileName String?
quantity Decimal
unit Unit? @relation(fields: [unitId], references: [id])
quantity Decimal?
unit Unit? @relation(fields: [unitId], references: [id])
unitId String?
price Int
subtotal Int
price Int?
subtotal Int?
linkUrl String
notes String?
reimbursementRequest Reimbursement_Request? @relation(fields: [reimbursementRequestId], references: [reimbursementRequestId])
reimbursementRequest Reimbursement_Request? @relation(fields: [reimbursementRequestId], references: [reimbursementRequestId])
reimbursementRequestId String?
reimbursementProducts Reimbursement_Product[]

@@index([assemblyId])
@@index([materialTypeId])
Expand Down
42 changes: 25 additions & 17 deletions src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2297,58 +2297,66 @@ const performSeed: () => Promise<void> = async () => {
'10k Resistor',
MaterialStatus.Ordered,
'Resistor',
'Digikey',
'abcdef',
new Decimal(20),
30,
600,
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
{
carNumber: 0,
projectNumber: 1,
workPackageNumber: 0
},
ner,
'Here are some notes'
'Digikey',
'abcdef',
new Decimal(20),
30,
600,
'Here are some notes',
assembly1.assemblyId,
undefined,
undefined,
undefined
);

await BillOfMaterialsService.createMaterial(
thomasEmrax,
'20k Resistor',
MaterialStatus.Ordered,
'Resistor',
'Digikey',
'bacfed',
new Decimal(10),
7,
70,
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
{
carNumber: 0,
projectNumber: 1,
workPackageNumber: 0
},
ner,
'Here are some more notes'
'Digikey',
'bacfed',
new Decimal(10),
7,
70,
'Here are some more notes',
undefined,
undefined,
undefined,
undefined
);

await BillOfMaterialsService.createMaterial(
thomasEmrax,
'100k Resistor',
MaterialStatus.ReadyToOrder,
'Resistor',
'Digikey',
'lalsd',
new Decimal(5),
10,
50,
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
{
carNumber: 0,
projectNumber: 1,
workPackageNumber: 0
},
ner,
'Digikey',
'lalsd',
new Decimal(5),
10,
50,
undefined,
undefined,
undefined,
Expand Down
11 changes: 11 additions & 0 deletions src/backend/src/routes/projects.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
validateInputs,
materialValidators
} from '../utils/validation.utils.js';
import { validateWBS } from 'shared';
import ProjectsController from '../controllers/projects.controllers.js';

const projectRouter = express.Router();
Expand Down Expand Up @@ -93,6 +94,16 @@ projectRouter.post(
);
projectRouter.post('/bom/material/:wbsNum/create', ...materialValidators, validateInputs, ProjectsController.createMaterial);
projectRouter.post('/bom/material/:materialId/edit', ...materialValidators, validateInputs, ProjectsController.editMaterial);
projectRouter.post(
'/bom/material/copy',
body('materialIds').isArray({ min: 1 }),
nonEmptyString(body('materialIds.*')),
body('destinationWbsNum').customSanitizer((value) => {
return validateWBS(value);
}),
validateInputs,
ProjectsController.copyMaterialsToProject
);

projectRouter.post(
'/bom/assembly/:assemblyId/edit',
Expand Down
5 changes: 3 additions & 2 deletions src/backend/src/routes/reimbursement-requests.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
isOptionalDate,
nonEmptyString,
validateInputs,
validateReimbursementProducts
validateReimbursementProducts,
validateReimbursementProductsForEdit
} from '../utils/validation.utils.js';
import ReimbursementRequestController from '../controllers/reimbursement-requests.controllers.js';
import multer, { memoryStorage } from 'multer';
Expand Down Expand Up @@ -139,7 +140,7 @@ reimbursementRequestsRouter.post(
nonEmptyString(body('receiptPictures.*.googleFileId')),
nonEmptyString(body('accountCodeId')),
intMinZero(body('totalCost')),
validateReimbursementProducts(),
validateReimbursementProductsForEdit(),
validateInputs,
ReimbursementRequestController.editReimbursementRequest
);
Expand Down
Loading
Loading