From 4d9e618b10249a2dd2f7253c79a95d1493d8d2a1 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Thu, 12 Feb 2026 01:30:09 -0500 Subject: [PATCH 01/15] 3881: added select material to copy modal --- .../BOM/MaterialForm/MaterialFormView.tsx | 28 +- .../SelectMaterialToCopyModal.tsx | 306 ++++++++++++++++++ 2 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 173258e005..6bb9b23b87 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -23,6 +23,7 @@ import { displayEnum } from '../../../../../utils/pipes'; import { MaterialStatus } from 'shared'; import React from 'react'; import { AddCircle } from '@mui/icons-material'; +import SelectMaterialToCopyModal from './SelectMaterialToCopyModal'; export interface MaterialFormViewProps { submitText: 'Add' | 'Edit'; @@ -72,6 +73,26 @@ const MaterialFormView: React.FC = ({ const price = watch('price'); const subtotal = quantity && price ? quantity * price : 0; + const [copyModalOpen, setCopyModalOpen] = React.useState(false); + + const handleCopySelect = (m: any) => { + setValue('name', m.name ?? ''); + setValue('status', m.status ?? MaterialStatus.Ordered); + setValue('materialTypeName', m.materialTypeName ?? ''); + setValue('manufacturerName', m.manufacturerName ?? ''); + setValue('manufacturerPartNumber', m.manufacturerPartNumber ?? ''); + setValue('pdmFileName', m.pdmFileName ?? ''); + setValue('linkUrl', m.linkUrl ?? ''); + setValue('quantity', m.quantity ?? undefined); + setValue('unitName', m.unitName ?? undefined); + setValue('price', m.price ?? undefined); + setValue('notes', m.notes ?? ''); + setValue('reimbursementRequestId', m.reimbursementRequest?.reimbursementRequestId ?? undefined); + setValue('assemblyId', undefined); + + setCopyModalOpen(false); + }; + return ( = ({ - )*/} + )} ); }; From 4c73b17dfd82ad22ebce65be85321415c46e4343 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Wed, 18 Mar 2026 23:46:59 -0400 Subject: [PATCH 05/15] linting/prettier checks --- .../SelectMaterialToCopyModal.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index 4e2a272476..1276231835 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -82,7 +82,9 @@ const SelectMaterialToCopyModal: React.FC = ({ o const projectsForSelectedCar = useMemo(() => { if (!selectedCar) return []; - const carNumber = selectedCar.wbsNum.carNumber; + const { + wbsNum: { carNumber } + } = selectedCar; return projects.filter((p) => p.wbsNum.carNumber === carNumber); }, [projects, selectedCar]); @@ -105,8 +107,9 @@ const SelectMaterialToCopyModal: React.FC = ({ o ['materials', 'car', selectedCar?.wbsNum.carNumber ?? 'none'], async () => { if (!selectedCar) return []; - const carNumber = selectedCar.wbsNum.carNumber; - + const { + wbsNum: { carNumber } + } = selectedCar; const projectsInCar = projects.filter((p) => p.wbsNum.carNumber === carNumber); const results = await Promise.all( projectsInCar.map(async (p) => { @@ -141,9 +144,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o const searchOptions = useMemo(() => { const q = searchText.trim().toLowerCase(); const filtered = - q.length === 0 - ? carSearchResults - : carSearchResults.filter(({ material }) => material.name.toLowerCase().includes(q)); + q.length === 0 ? carSearchResults : carSearchResults.filter(({ material }) => material.name.toLowerCase().includes(q)); return filtered.map(searchResultToOption); }, [carSearchResults, searchText]); @@ -175,10 +176,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o }, [open, reset]); const anyLoading = - carsQuery.isLoading || - projectsQuery.isLoading || - projectMaterialsQuery.isLoading || - carMaterialsQuery.isLoading; + carsQuery.isLoading || projectsQuery.isLoading || projectMaterialsQuery.isLoading || carMaterialsQuery.isLoading; const anyError = (carsQuery.error as Error | undefined) || @@ -277,7 +275,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o options={carOptions} value={selectedCarOption} onChange={(_, value) => { - const next = value ? cars.find((c) => c.wbsElementId === value.id) ?? null : null; + const next = value ? (cars.find((c) => c.wbsElementId === value.id) ?? null) : null; setSelectedCar(next); }} required={true} @@ -291,7 +289,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o options={projectOptions} value={selectedProjectOption} onChange={(_, value) => { - const next = value ? projectsForSelectedCar.find((p) => p.wbsElementId === value.id) ?? null : null; + const next = value ? (projectsForSelectedCar.find((p) => p.wbsElementId === value.id) ?? null) : null; setSelectedProject(next); }} required={true} @@ -305,7 +303,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o options={projectMaterialOptions} value={selectedMaterialOption} onChange={(_, value) => { - const next = value ? projectMaterials.find((m) => m.materialId === value.id) ?? null : null; + const next = value ? (projectMaterials.find((m) => m.materialId === value.id) ?? null) : null; setSelectedMaterial(next); }} required={true} @@ -322,4 +320,4 @@ const SelectMaterialToCopyModal: React.FC = ({ o ); }; -export default SelectMaterialToCopyModal; \ No newline at end of file +export default SelectMaterialToCopyModal; From e0b6ba6da3878b8b0223566d42a3ab084d6ad1dd Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Thu, 19 Mar 2026 00:11:33 -0400 Subject: [PATCH 06/15] lint issues --- .../ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 131b43433f..7a5ffde584 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -93,7 +93,6 @@ const MaterialFormView: React.FC = ({ setValue('unitName', m.unitName ?? undefined); setValue('price', m.price ?? undefined); setValue('notes', m.notes ?? ''); - setValue('reimbursementRequestId', m.reimbursementRequest?.reimbursementRequestId ?? undefined); setValue('assemblyId', undefined); setCopyModalOpen(false); From 7edfe314122259ffea735f0799d43e50d22a0ded Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Thu, 19 Mar 2026 01:31:54 -0400 Subject: [PATCH 07/15] merge conflicts resolved --- .../BOM/BOMTableWrapper.tsx | 21 ++++--------------- .../BOM/MaterialForm/MaterialFormView.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx index 381d236a65..e8e4756aa3 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx @@ -11,8 +11,7 @@ import { useToast } from '../../../../hooks/toasts.hooks'; import { useAssignMaterialToAssembly, useDeleteAssembly, useDeleteMaterial } from '../../../../hooks/bom.hooks'; import LoadingIndicator from '../../../../components/LoadingIndicator'; import EditMaterialModal from './MaterialForm/EditMaterialModal'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { Button, Link, Tooltip, Typography } from '@mui/material'; +import { Button, Link, Typography } from '@mui/material'; import { bomBaseColDef } from '../../../../utils/bom.utils'; import NERModal from '../../../../components/NERModal'; import { renderStatusBOM } from './BOMTableCustomCells'; @@ -298,22 +297,10 @@ const BOMTableWrapper: React.FC = ({ flex: 1.5, field: 'name', headerName: 'Name', + type: 'string', sortable: false, filterable: false, - hide: hideColumn[3], - renderCell: (params) => { - const material = materials.find((m) => m.materialId === params.row.materialId); - return ( - - {params.value} - {material?.isCopied && ( - - - - )} - - ); - } + hide: hideColumn[3] }, { ...bomBaseColDef, @@ -427,4 +414,4 @@ const BOMTableWrapper: React.FC = ({ ); }; -export default BOMTableWrapper; +export default BOMTableWrapper; \ No newline at end of file diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 7a5ffde584..b83b0f6226 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -91,7 +91,7 @@ const MaterialFormView: React.FC = ({ setValue('linkUrl', m.linkUrl ?? ''); setValue('quantity', m.quantity ?? undefined); setValue('unitName', m.unitName ?? undefined); - setValue('price', m.price ?? undefined); + setValue('price', m.price != null ? m.price / 100 : undefined); setValue('notes', m.notes ?? ''); setValue('assemblyId', undefined); @@ -548,6 +548,11 @@ const MaterialFormView: React.FC = ({ )} + setCopyModalOpen(false)} + onSelect={handleCopySelect} + /> ); }; From 7f05c72f1af5ad7a92ff54f8481a6e05c588d16b Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Thu, 19 Mar 2026 01:40:04 -0400 Subject: [PATCH 08/15] more lint/prettier --- .../ProjectViewContainer/BOM/BOMTableWrapper.tsx | 2 +- .../BOM/MaterialForm/MaterialFormView.tsx | 6 +----- .../BOM/MaterialForm/SelectMaterialToCopyModal.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx index e8e4756aa3..c9afa898dc 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx @@ -414,4 +414,4 @@ const BOMTableWrapper: React.FC = ({ ); }; -export default BOMTableWrapper; \ No newline at end of file +export default BOMTableWrapper; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index b83b0f6226..bd1574bf09 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -548,11 +548,7 @@ const MaterialFormView: React.FC = ({ )} - setCopyModalOpen(false)} - onSelect={handleCopySelect} - /> + setCopyModalOpen(false)} onSelect={handleCopySelect} /> ); }; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index 1276231835..0cefd1bda5 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -75,8 +75,8 @@ const SelectMaterialToCopyModal: React.FC = ({ o const carsQuery = useGetAllCars(); const projectsQuery = useAllProjects(); - const cars = carsQuery.data ?? []; - const projects = projectsQuery.data ?? []; + const cars = useMemo(() => carsQuery.data ?? [], [carsQuery.data]); + const projects = useMemo(() => projectsQuery.data ?? [], [projectsQuery.data]); const latestCar = useMemo(() => getLatestCar(cars), [cars]); @@ -134,8 +134,8 @@ const SelectMaterialToCopyModal: React.FC = ({ o { enabled: !!selectedCar && open } ); - const projectMaterials = projectMaterialsQuery.data ?? []; - const carSearchResults = carMaterialsQuery.data ?? []; + const projectMaterials = useMemo(() => projectMaterialsQuery.data ?? [], [projectMaterialsQuery.data]); + const carSearchResults = useMemo(() => carMaterialsQuery.data ?? [], [carMaterialsQuery.data]); const carOptions = useMemo(() => cars.map(carToOption), [cars]); const projectOptions = useMemo(() => projectsForSelectedCar.map(projectToOption), [projectsForSelectedCar]); From 650bcd0e76b68352c02d63d1952f75bc59fc1721 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Mon, 23 Mar 2026 15:29:12 -0400 Subject: [PATCH 09/15] material form fixes --- .../BOM/MaterialForm/MaterialForm.tsx | 7 +++++-- .../BOM/MaterialForm/MaterialFormView.tsx | 16 ++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx index a815c2983c..9311cd5e51 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx @@ -29,7 +29,8 @@ const schema = yup.object().shape({ linkUrl: yup.string().optional(), notes: yup.string().optional(), pdmFileName: yup.string().optional(), - assemblyId: yup.string().optional() + assemblyId: yup.string().optional(), + reimbursementRequestId: yup.string().optional() }); export interface MaterialFormInput { @@ -45,6 +46,7 @@ export interface MaterialFormInput { linkUrl?: string; notes?: string; assemblyId?: string; + reimbursementRequestId?: string; } export interface MaterialDataSubmission { @@ -101,7 +103,8 @@ const MaterialForm: React.FC = ({ unitName: defaultValues?.unitName, linkUrl: defaultValues?.linkUrl ?? '', notes: defaultValues?.notes, - assemblyId: defaultValues?.assemblyId + assemblyId: defaultValues?.assemblyId, + reimbursementRequestId: defaultValues?.reimbursementRequestId }, resolver: yupResolver(schema) }); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index bd1574bf09..58b9fa774f 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -15,7 +15,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue, UseFormWatch } from 'react-hook-form'; -import { Assembly, Manufacturer, MaterialType, Unit } from 'shared'; +import { Assembly, Manufacturer, Material, MaterialType, Unit } from 'shared'; import ReactHookTextField from '../../../../../components/ReactHookTextField'; import { MaterialFormInput } from './MaterialForm'; import NERFormModal from '../../../../../components/NERFormModal'; @@ -44,7 +44,6 @@ export interface MaterialFormViewProps { watch: UseFormWatch; createManufacturer: (name: string) => void; setValue: UseFormSetValue; - copyFromExistingBomAction?: React.ReactNode; fromRRForm?: boolean; } @@ -81,19 +80,19 @@ const MaterialFormView: React.FC = ({ const [copyModalOpen, setCopyModalOpen] = React.useState(false); - const handleCopySelect = (m: any) => { + const handleCopySelect = (m: Material) => { setValue('name', m.name ?? ''); - setValue('status', m.status ?? MaterialStatus.Ordered); setValue('materialTypeName', m.materialTypeName ?? ''); setValue('manufacturerName', m.manufacturerName ?? ''); setValue('manufacturerPartNumber', m.manufacturerPartNumber ?? ''); setValue('pdmFileName', m.pdmFileName ?? ''); setValue('linkUrl', m.linkUrl ?? ''); - setValue('quantity', m.quantity ?? undefined); + setValue('quantity', m.quantity != null ? Number(m.quantity) : undefined); setValue('unitName', m.unitName ?? undefined); setValue('price', m.price != null ? m.price / 100 : undefined); setValue('notes', m.notes ?? ''); setValue('assemblyId', undefined); + setValue('reimbursementRequestId', undefined); setCopyModalOpen(false); }; @@ -548,7 +547,12 @@ const MaterialFormView: React.FC = ({ )} - setCopyModalOpen(false)} onSelect={handleCopySelect} /> + setCopyModalOpen(false)} + onSelect={handleCopySelect} + assemblies={assemblies ?? []} + /> ); }; From bcdb591ca07096c20320b569e6b0446e64fd60ba Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Mon, 23 Mar 2026 16:11:24 -0400 Subject: [PATCH 10/15] review request changes --- .../BOM/BOMTableWrapper.tsx | 18 ++++++- .../BOM/MaterialForm/MaterialFormView.tsx | 8 +-- .../SelectMaterialToCopyModal.tsx | 50 +++++++++++++------ 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx index c9afa898dc..4345afcf96 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/BOMTableWrapper.tsx @@ -11,7 +11,8 @@ import { useToast } from '../../../../hooks/toasts.hooks'; import { useAssignMaterialToAssembly, useDeleteAssembly, useDeleteMaterial } from '../../../../hooks/bom.hooks'; import LoadingIndicator from '../../../../components/LoadingIndicator'; import EditMaterialModal from './MaterialForm/EditMaterialModal'; -import { Button, Link, Typography } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { Button, Link, Tooltip, Typography } from '@mui/material'; import { bomBaseColDef } from '../../../../utils/bom.utils'; import NERModal from '../../../../components/NERModal'; import { renderStatusBOM } from './BOMTableCustomCells'; @@ -300,7 +301,20 @@ const BOMTableWrapper: React.FC = ({ type: 'string', sortable: false, filterable: false, - hide: hideColumn[3] + hide: hideColumn[3], + renderCell: (params) => { + const material = materials.find((m) => m.materialId === params.row.materialId); + return ( + + {params.value} + {material?.isCopied && ( + + + + )} + + ); + } }, { ...bomBaseColDef, diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 58b9fa774f..8eeb756387 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -547,10 +547,10 @@ const MaterialFormView: React.FC = ({ )} - setCopyModalOpen(false)} - onSelect={handleCopySelect} + setCopyModalOpen(false)} + onSelect={handleCopySelect} assemblies={assemblies ?? []} /> diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index 0cefd1bda5..f16c463b48 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Autocomplete, Box, CircularProgress, Stack, TextField, Typography } from '@mui/material'; +import { Autocomplete, Box, CircularProgress, InputAdornment, Stack, TextField, Typography } from '@mui/material'; import { useForm } from 'react-hook-form'; import { useQuery } from 'react-query'; -import { Car, Material, ProjectPreview, WbsNumber } from 'shared'; +import { Assembly, Car, Material, ProjectPreview, WbsNumber } from 'shared'; import NERFormModal from '../../../../../components/NERFormModal'; import NERAutocomplete from '../../../../../components/NERAutocomplete'; @@ -10,6 +10,7 @@ import NERAutocomplete from '../../../../../components/NERAutocomplete'; import { useGetAllCars } from '../../../../../hooks/cars.hooks'; import { useAllProjects } from '../../../../../hooks/projects.hooks'; import { getMaterialsForWbsElement } from '../../../../../apis/bom.api'; +import SearchIcon from '@mui/icons-material/Search'; type AutocompleteOption = { label: string; id: string }; @@ -22,12 +23,13 @@ interface SelectMaterialToCopyModalProps { open: boolean; onHide: () => void; onSelect: (material: Material) => void; + assemblies: Assembly[]; } type FormValues = Record; const carToOption = (car: Car): AutocompleteOption => ({ - label: String(car.wbsNum.carNumber), + label: `Car ${car.wbsNum.carNumber} - ${car.name}`, id: car.wbsElementId }); @@ -36,18 +38,6 @@ const projectToOption = (project: ProjectPreview): AutocompleteOption => ({ id: project.wbsElementId }); -const materialToOption = (material: Material): AutocompleteOption => ({ - label: [ - material.name, - material.manufacturerName, - material.materialTypeName, - material.assemblyId ? `Assembly: ${material.assemblyId}` : undefined - ] - .filter(Boolean) - .join(' – '), - id: material.materialId -}); - const searchResultToOption = ({ material, project }: SearchResult): AutocompleteOption => ({ label: `${material.name} – ${project.wbsNum.carNumber}.${project.wbsNum.projectNumber} - ${project.name}`, id: material.materialId @@ -64,7 +54,7 @@ const getLatestCar = (cars: Car[]): Car | null => { return [...cars].sort((a, b) => b.wbsNum.carNumber - a.wbsNum.carNumber)[0]; }; -const SelectMaterialToCopyModal: React.FC = ({ open, onHide, onSelect }) => { +const SelectMaterialToCopyModal: React.FC = ({ open, onHide, onSelect, assemblies }) => { const { reset, handleSubmit } = useForm(); const [selectedCar, setSelectedCar] = useState(null); @@ -93,6 +83,23 @@ const SelectMaterialToCopyModal: React.FC = ({ o return projectToProjectWbs(selectedProject); }, [selectedProject]); + const assemblyNameById = useMemo( + () => new Map(assemblies.map((assembly) => [assembly.assemblyId, assembly.name])), + [assemblies] + ); + + const materialToOption = (material: Material): AutocompleteOption => ({ + label: [ + material.name, + material.manufacturerName, + material.materialTypeName, + material.assemblyId ? `Assembly: ${assemblyNameById.get(material.assemblyId) ?? material.assemblyId}` : undefined + ] + .filter(Boolean) + .join(' – '), + id: material.materialId + }); + const projectMaterialsQuery = useQuery( ['materials', 'project', selectedProject?.wbsElementId ?? 'none'], async () => { @@ -263,6 +270,17 @@ const SelectMaterialToCopyModal: React.FC = ({ o {...params} placeholder={selectedCar ? 'Search materials by name…' : 'Select a car first'} fullWidth + InputProps={{ + ...params.InputProps, + startAdornment: ( + <> + + + + {params.InputProps.startAdornment} + + ) + }} /> )} /> From 18c9373521ecf7f23e2d9d11710e65cbef92de43 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Mon, 23 Mar 2026 16:23:01 -0400 Subject: [PATCH 11/15] linting issues --- .../SelectMaterialToCopyModal.tsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index f16c463b48..7a844ac607 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Autocomplete, Box, CircularProgress, InputAdornment, Stack, TextField, Typography } from '@mui/material'; import { useForm } from 'react-hook-form'; import { useQuery } from 'react-query'; @@ -88,17 +88,20 @@ const SelectMaterialToCopyModal: React.FC = ({ o [assemblies] ); - const materialToOption = (material: Material): AutocompleteOption => ({ - label: [ - material.name, - material.manufacturerName, - material.materialTypeName, - material.assemblyId ? `Assembly: ${assemblyNameById.get(material.assemblyId) ?? material.assemblyId}` : undefined - ] - .filter(Boolean) - .join(' – '), - id: material.materialId - }); + const materialToOption = useCallback( + (material: Material): AutocompleteOption => ({ + label: [ + material.name, + material.manufacturerName, + material.materialTypeName, + material.assemblyId ? `Assembly: ${assemblyNameById.get(material.assemblyId) ?? material.assemblyId}` : undefined + ] + .filter(Boolean) + .join(' – '), + id: material.materialId + }), + [assemblyNameById] + ); const projectMaterialsQuery = useQuery( ['materials', 'project', selectedProject?.wbsElementId ?? 'none'], @@ -146,7 +149,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o const carOptions = useMemo(() => cars.map(carToOption), [cars]); const projectOptions = useMemo(() => projectsForSelectedCar.map(projectToOption), [projectsForSelectedCar]); - const projectMaterialOptions = useMemo(() => projectMaterials.map(materialToOption), [projectMaterials]); + const projectMaterialOptions = useMemo(() => projectMaterials.map(materialToOption), [projectMaterials, materialToOption]); const searchOptions = useMemo(() => { const q = searchText.trim().toLowerCase(); From 49c6eeb7040cf30c193ddd6970849cf34d2b8e79 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Mon, 23 Mar 2026 16:26:20 -0400 Subject: [PATCH 12/15] prettier checks --- .../BOM/MaterialForm/SelectMaterialToCopyModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index 7a844ac607..ae31ebb13c 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -90,15 +90,15 @@ const SelectMaterialToCopyModal: React.FC = ({ o const materialToOption = useCallback( (material: Material): AutocompleteOption => ({ - label: [ + label: [ material.name, material.manufacturerName, material.materialTypeName, material.assemblyId ? `Assembly: ${assemblyNameById.get(material.assemblyId) ?? material.assemblyId}` : undefined - ] + ] .filter(Boolean) .join(' – '), - id: material.materialId + id: material.materialId }), [assemblyNameById] ); From a26582b314f8e720bb6241ea6cf8787083e347d3 Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Tue, 24 Mar 2026 23:23:55 -0400 Subject: [PATCH 13/15] removed unnecessary changes --- .../ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx index 9311cd5e51..a815c2983c 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialForm.tsx @@ -29,8 +29,7 @@ const schema = yup.object().shape({ linkUrl: yup.string().optional(), notes: yup.string().optional(), pdmFileName: yup.string().optional(), - assemblyId: yup.string().optional(), - reimbursementRequestId: yup.string().optional() + assemblyId: yup.string().optional() }); export interface MaterialFormInput { @@ -46,7 +45,6 @@ export interface MaterialFormInput { linkUrl?: string; notes?: string; assemblyId?: string; - reimbursementRequestId?: string; } export interface MaterialDataSubmission { @@ -103,8 +101,7 @@ const MaterialForm: React.FC = ({ unitName: defaultValues?.unitName, linkUrl: defaultValues?.linkUrl ?? '', notes: defaultValues?.notes, - assemblyId: defaultValues?.assemblyId, - reimbursementRequestId: defaultValues?.reimbursementRequestId + assemblyId: defaultValues?.assemblyId }, resolver: yupResolver(schema) }); From 11bdb51a3cd699b19e5e53bcb77b68714ea0969a Mon Sep 17 00:00:00 2001 From: Samuel Shrestha Date: Wed, 25 Mar 2026 00:26:58 -0400 Subject: [PATCH 14/15] final touches --- .../ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx | 1 - .../BOM/MaterialForm/SelectMaterialToCopyModal.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx index 8eeb756387..fc3b89c531 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/MaterialFormView.tsx @@ -92,7 +92,6 @@ const MaterialFormView: React.FC = ({ setValue('price', m.price != null ? m.price / 100 : undefined); setValue('notes', m.notes ?? ''); setValue('assemblyId', undefined); - setValue('reimbursementRequestId', undefined); setCopyModalOpen(false); }; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index ae31ebb13c..58ae1343aa 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -271,7 +271,7 @@ const SelectMaterialToCopyModal: React.FC = ({ o renderInput={(params) => ( Date: Sun, 29 Mar 2026 20:30:15 -0400 Subject: [PATCH 15/15] #3881 requested changes --- src/frontend/src/hooks/bom.hooks.ts | 32 +- .../SelectMaterialToCopyModal.tsx | 385 +++++++----------- 2 files changed, 189 insertions(+), 228 deletions(-) diff --git a/src/frontend/src/hooks/bom.hooks.ts b/src/frontend/src/hooks/bom.hooks.ts index 85539f970e..b078517aa3 100644 --- a/src/frontend/src/hooks/bom.hooks.ts +++ b/src/frontend/src/hooks/bom.hooks.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { Assembly, Manufacturer, Material, MaterialType, Unit, WbsNumber, wbsPipe } from 'shared'; +import { Assembly, Manufacturer, Material, MaterialType, ProjectPreview, Unit, WbsNumber, wbsPipe } from 'shared'; import { useToast } from '../hooks/toasts.hooks'; import { assignMaterialToAssembly, @@ -326,3 +326,33 @@ export const useGetMaterialsForWbsElement = (wbsNum: WbsNumber) => { return data; }); }; + +export const useGetMaterialsForCar = (carNumber: number | null, projects: ProjectPreview[]) => { + const projectsInCar = projects.filter((p) => p.wbsNum.carNumber === carNumber); + + return useQuery( + ['materials', 'car', carNumber ?? 'none'], + async () => { + const results = await Promise.all( + projectsInCar.map(async (p) => { + const { data } = await getMaterialsForWbsElement({ + carNumber: p.wbsNum.carNumber, + projectNumber: p.wbsNum.projectNumber, + workPackageNumber: 0 + }); + return data; + }) + ); + + const flat = results.flat(); + const seen = new Set(); + return flat.filter((material) => { + const key = `${material.name.toLowerCase()}-${material.assemblyId ?? 'no-assembly'}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + }, + { enabled: carNumber !== null && projectsInCar.length > 0 } + ); +}; diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx index 58ae1343aa..7aece296e1 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/BOM/MaterialForm/SelectMaterialToCopyModal.tsx @@ -1,24 +1,20 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Autocomplete, Box, CircularProgress, InputAdornment, Stack, TextField, Typography } from '@mui/material'; +import React, { useCallback, useMemo, useState } from 'react'; +import { Autocomplete, InputAdornment, Stack, TextField, Typography } from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; import { useForm } from 'react-hook-form'; -import { useQuery } from 'react-query'; import { Assembly, Car, Material, ProjectPreview, WbsNumber } from 'shared'; import NERFormModal from '../../../../../components/NERFormModal'; import NERAutocomplete from '../../../../../components/NERAutocomplete'; +import LoadingIndicator from '../../../../../components/LoadingIndicator'; +import ErrorPage from '../../../../ErrorPage'; import { useGetAllCars } from '../../../../../hooks/cars.hooks'; import { useAllProjects } from '../../../../../hooks/projects.hooks'; -import { getMaterialsForWbsElement } from '../../../../../apis/bom.api'; -import SearchIcon from '@mui/icons-material/Search'; +import { useGetMaterialsForWbsElement, useGetMaterialsForCar } from '../../../../../hooks/bom.hooks'; type AutocompleteOption = { label: string; id: string }; -type SearchResult = { - material: Material; - project: ProjectPreview; -}; - interface SelectMaterialToCopyModalProps { open: boolean; onHide: () => void; @@ -38,12 +34,7 @@ const projectToOption = (project: ProjectPreview): AutocompleteOption => ({ id: project.wbsElementId }); -const searchResultToOption = ({ material, project }: SearchResult): AutocompleteOption => ({ - label: `${material.name} – ${project.wbsNum.carNumber}.${project.wbsNum.projectNumber} - ${project.name}`, - id: material.materialId -}); - -const projectToProjectWbs = (project: ProjectPreview): WbsNumber => ({ +const projectToWbsNumber = (project: ProjectPreview): WbsNumber => ({ carNumber: project.wbsNum.carNumber, projectNumber: project.wbsNum.projectNumber, workPackageNumber: 0 @@ -60,34 +51,45 @@ const SelectMaterialToCopyModal: React.FC = ({ o const [selectedCar, setSelectedCar] = useState(null); const [selectedProject, setSelectedProject] = useState(null); const [selectedMaterial, setSelectedMaterial] = useState(null); - const [searchText, setSearchText] = useState(''); - const carsQuery = useGetAllCars(); - const projectsQuery = useAllProjects(); + const { data: cars, isLoading: carsIsLoading, isError: carsIsError, error: carsError } = useGetAllCars(); + + const { data: projects, isLoading: projectsIsLoading, isError: projectsIsError, error: projectsError } = useAllProjects(); - const cars = useMemo(() => carsQuery.data ?? [], [carsQuery.data]); - const projects = useMemo(() => projectsQuery.data ?? [], [projectsQuery.data]); + const allCars = useMemo(() => cars ?? [], [cars]); + const allProjects = useMemo(() => projects ?? [], [projects]); - const latestCar = useMemo(() => getLatestCar(cars), [cars]); + const latestCar = useMemo(() => getLatestCar(allCars), [allCars]); + const effectiveCar = selectedCar ?? latestCar; const projectsForSelectedCar = useMemo(() => { - if (!selectedCar) return []; - const { - wbsNum: { carNumber } - } = selectedCar; - return projects.filter((p) => p.wbsNum.carNumber === carNumber); - }, [projects, selectedCar]); - - const selectedProjectWbsNum = useMemo(() => { - if (!selectedProject) return null; - return projectToProjectWbs(selectedProject); - }, [selectedProject]); - - const assemblyNameById = useMemo( - () => new Map(assemblies.map((assembly) => [assembly.assemblyId, assembly.name])), - [assemblies] + if (!effectiveCar) return []; + return allProjects.filter((p) => p.wbsNum.carNumber === effectiveCar.wbsNum.carNumber); + }, [allProjects, effectiveCar]); + + const selectedProjectWbsNum = useMemo( + () => (selectedProject ? projectToWbsNumber(selectedProject) : null), + [selectedProject] ); + // Materials for the selected project for autocomplete + const { + data: projectMaterials, + isLoading: projectMaterialsIsLoading, + isError: projectMaterialsIsError, + error: projectMaterialsError + } = useGetMaterialsForWbsElement(selectedProjectWbsNum ?? { carNumber: 0, projectNumber: 0, workPackageNumber: 0 }); + + // All materials across the selected car for search bar + const { + data: carMaterials, + isLoading: carMaterialsIsLoading, + isError: carMaterialsIsError, + error: carMaterialsError + } = useGetMaterialsForCar(effectiveCar?.wbsNum.carNumber ?? null, allProjects); + + const assemblyNameById = useMemo(() => new Map(assemblies.map((a) => [a.assemblyId, a.name])), [assemblies]); + const materialToOption = useCallback( (material: Material): AutocompleteOption => ({ label: [ @@ -103,191 +105,111 @@ const SelectMaterialToCopyModal: React.FC = ({ o [assemblyNameById] ); - const projectMaterialsQuery = useQuery( - ['materials', 'project', selectedProject?.wbsElementId ?? 'none'], - async () => { - if (!selectedProjectWbsNum) return []; - const { data } = await getMaterialsForWbsElement(selectedProjectWbsNum); - return data; - }, - { enabled: !!selectedProjectWbsNum && open } - ); + const materials = useMemo(() => (selectedProject ? (projectMaterials ?? []) : []), [selectedProject, projectMaterials]); - const carMaterialsQuery = useQuery( - ['materials', 'car', selectedCar?.wbsNum.carNumber ?? 'none'], - async () => { - if (!selectedCar) return []; - const { - wbsNum: { carNumber } - } = selectedCar; - const projectsInCar = projects.filter((p) => p.wbsNum.carNumber === carNumber); - const results = await Promise.all( - projectsInCar.map(async (p) => { - const { data } = await getMaterialsForWbsElement(projectToProjectWbs(p)); - return data.map((material) => ({ - material, - project: p - })); - }) - ); - - const flattened = results.flat(); - const seen = new Set(); - - return flattened.filter(({ material, project }) => { - const key = `${material.name.toLowerCase()}-${project.name.toLowerCase()}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - }, - { enabled: !!selectedCar && open } - ); - - const projectMaterials = useMemo(() => projectMaterialsQuery.data ?? [], [projectMaterialsQuery.data]); - const carSearchResults = useMemo(() => carMaterialsQuery.data ?? [], [carMaterialsQuery.data]); - - const carOptions = useMemo(() => cars.map(carToOption), [cars]); + const carOptions = useMemo(() => allCars.map(carToOption), [allCars]); const projectOptions = useMemo(() => projectsForSelectedCar.map(projectToOption), [projectsForSelectedCar]); - const projectMaterialOptions = useMemo(() => projectMaterials.map(materialToOption), [projectMaterials, materialToOption]); - - const searchOptions = useMemo(() => { - const q = searchText.trim().toLowerCase(); - const filtered = - q.length === 0 ? carSearchResults : carSearchResults.filter(({ material }) => material.name.toLowerCase().includes(q)); - - return filtered.map(searchResultToOption); - }, [carSearchResults, searchText]); + const materialOptions = useMemo(() => materials.map(materialToOption), [materials, materialToOption]); + const carMaterialOptions = useMemo(() => (carMaterials ?? []).map(materialToOption), [carMaterials, materialToOption]); - useEffect(() => { - if (open && !selectedCar && latestCar) { - setSelectedCar(latestCar); - } - }, [open, selectedCar, latestCar]); - - useEffect(() => { - setSelectedProject(null); - setSelectedMaterial(null); - setSearchText(''); - }, [selectedCar?.wbsElementId]); + const selectedCarOption = effectiveCar ? (carOptions.find((o) => o.id === effectiveCar.wbsElementId) ?? null) : null; + const selectedProjectOption = selectedProject ? projectToOption(selectedProject) : null; + const selectedMaterialOption = selectedMaterial ? materialToOption(selectedMaterial) : null; - useEffect(() => { - setSelectedMaterial(null); - }, [selectedProject?.wbsElementId]); + // Selecting from the search bar auto-populates the project and material dropdowns + const handleSearchSelect = useCallback( + (_: React.SyntheticEvent, value: AutocompleteOption | null) => { + if (!value) return; + const material = (carMaterials ?? []).find((m) => m.materialId === value.id) ?? null; + if (!material) return; + const project = allProjects.find((p) => p.wbsElementId === material.wbsElementId) ?? null; + setSelectedProject(project); + setSelectedMaterial(material); + }, + [carMaterials, allProjects] + ); - useEffect(() => { - if (!open) { - setSelectedCar(null); + const handleCarChange = useCallback( + (_: React.SyntheticEvent, value: AutocompleteOption | null) => { + const next = value ? (allCars.find((c) => c.wbsElementId === value.id) ?? null) : null; + setSelectedCar(next); setSelectedProject(null); setSelectedMaterial(null); - setSearchText(''); - reset(); - } - }, [open, reset]); - - const anyLoading = - carsQuery.isLoading || projectsQuery.isLoading || projectMaterialsQuery.isLoading || carMaterialsQuery.isLoading; - - const anyError = - (carsQuery.error as Error | undefined) || - (projectsQuery.error as Error | undefined) || - projectMaterialsQuery.error || - carMaterialsQuery.error || - null; - - const selectedCarOption = selectedCar ? carToOption(selectedCar) : null; - const selectedProjectOption = selectedProject ? projectToOption(selectedProject) : null; - const selectedMaterialOption = selectedMaterial ? materialToOption(selectedMaterial) : null; + }, + [allCars] + ); - const canSubmit = !!selectedMaterial; + const handleProjectChange = useCallback( + (_: React.SyntheticEvent, value: AutocompleteOption | null) => { + const next = value ? (projectsForSelectedCar.find((p) => p.wbsElementId === value.id) ?? null) : null; + setSelectedProject(next); + setSelectedMaterial(null); + }, + [projectsForSelectedCar] + ); + + const handleMaterialChange = useCallback( + (_: React.SyntheticEvent, value: AutocompleteOption | null) => { + const next = value ? (materials.find((m) => m.materialId === value.id) ?? null) : null; + setSelectedMaterial(next); + }, + [materials] + ); const handleCopy = () => { if (!selectedMaterial) return; onSelect(selectedMaterial); onHide(); + reset(); + setSelectedCar(null); + setSelectedProject(null); + setSelectedMaterial(null); }; - return ( - - - {anyLoading && ( - - - Loading… - - )} + const handleHide = () => { + onHide(); + reset(); + setSelectedCar(null); + setSelectedProject(null); + setSelectedMaterial(null); + }; - {anyError && ( - - {anyError.message} - - )} + const modalContent = () => { + if (carsIsError) return ; + if (projectsIsError) return ; + if (projectMaterialsIsError) return ; + if (carMaterialsIsError) return ; + if (carsIsLoading || projectsIsLoading) return ; - - option.label} - onInputChange={(_, value) => { - setSearchText(value); - }} - onChange={async (_, value) => { - if (!value) return; - - const match = - carSearchResults.find(({ material, project }) => { - return ( - material.materialId === value.id && - `${material.name} – ${project.wbsNum.carNumber}.${project.wbsNum.projectNumber} - ${project.name}` === - value.label - ); - }) ?? null; - - if (!match) return; - - setSelectedProject(match.project); - setSearchText(match.material.name); - - const { data } = await getMaterialsForWbsElement(projectToProjectWbs(match.project)); - const selected = data.find((m) => m.materialId === match.material.materialId) ?? null; - - setSelectedMaterial(selected); - }} - disabled={!selectedCar || carMaterialsQuery.isLoading} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - + return ( + + option.label} + onChange={handleSearchSelect} + disabled={!effectiveCar || carMaterialsIsLoading} + renderInput={(params) => ( + + + + + {params.InputProps.startAdornment} + + ) + }} + /> + )} + /> = ({ o placeholder="Select a car" options={carOptions} value={selectedCarOption} - onChange={(_, value) => { - const next = value ? (cars.find((c) => c.wbsElementId === value.id) ?? null) : null; - setSelectedCar(next); - }} - required={true} - disabled={carsQuery.isLoading} + onChange={handleCarChange} + required /> { - const next = value ? (projectsForSelectedCar.find((p) => p.wbsElementId === value.id) ?? null) : null; - setSelectedProject(next); - }} - required={true} - disabled={!selectedCar || projectsQuery.isLoading} + onChange={handleProjectChange} + required + disabled={!effectiveCar} /> { - const next = value ? (projectMaterials.find((m) => m.materialId === value.id) ?? null) : null; - setSelectedMaterial(next); - }} - required={true} - disabled={!selectedProject || projectMaterialsQuery.isLoading} + onChange={handleMaterialChange} + required + disabled={!selectedProject || projectMaterialsIsLoading} /> - {!canSubmit && ( + {!selectedMaterial && ( - Pick a material to enable “Copy”. + Pick a material to enable "Copy". )} + ); + }; + + return ( + + {modalContent()} ); };