@@ -27,15 +27,16 @@ import {
2727 ReimbursementProductFormArgs ,
2828 IndexCode ,
2929 CreateRefundSourceArgs ,
30+ Material ,
3031 ProjectPreview
3132} from 'shared' ;
3233import { RemoveCircleOutline , AddCircleOutline } from '@mui/icons-material' ;
3334import { Control , Controller , FieldErrors , UseFormRegister , UseFormSetValue , UseFormWatch } from 'react-hook-form' ;
3435import { ReimbursementRequestFormInput } from './ReimbursementRequestForm' ;
3536import { useTheme } from '@mui/system' ;
36- import { useEffect , useState , useRef } from 'react' ;
37+ import { useEffect , useState , useRef , useMemo } from 'react' ;
3738import { useGetAllOtherProductReason } from '../../../hooks/finance.hooks' ;
38- import { useGetAssembliesForWbsElement } from '../../../hooks/bom.hooks' ;
39+ import { useGetMaterialsForWbsElement , useGetAssembliesForWbsElement } from '../../../hooks/bom.hooks' ;
3940import LoadingIndicator from '../../../components/LoadingIndicator' ;
4041import ErrorPage from '../../ErrorPage' ;
4142import { formatReasonName } from '../../../utils/reimbursement-request.utils' ;
@@ -66,6 +67,81 @@ const ListItem = styled('li')(({ theme }) => ({
6667 margin : theme . spacing ( 0.5 )
6768} ) ) ;
6869
70+ const MaterialAutocomplete : React . FC < {
71+ wbsNum : WbsNumber ;
72+ onSelect : ( material : Material | null ) => void ;
73+ initialValue ?: string ;
74+ } > = ( { wbsNum, onSelect, initialValue } ) => {
75+ const { data : materials , isLoading, isError, error } = useGetMaterialsForWbsElement ( wbsNum ) ;
76+ const [ materialSelected , setMaterialSelected ] = useState < { id : string ; label : string } | null > ( null ) ;
77+
78+ const materialOptions = useMemo (
79+ ( ) =>
80+ ( materials ?? [ ] ) . map ( ( material ) => ( {
81+ id : material . materialId ,
82+ label : `${ material . name } (${ material . materialTypeName } ): ${ material . manufacturerName ?? 'N/A' } , ${ material . manufacturerPartNumber ?? 'N/A' } `
83+ } ) ) ,
84+ [ materials ]
85+ ) ;
86+
87+ useEffect ( ( ) => {
88+ if ( ! materials || ! initialValue ) return ;
89+
90+ // Fetch pre-existing label
91+ let match = materialOptions . find ( ( o ) => o . label === initialValue ) ?? null ;
92+
93+ // Otherwise fetch new material by name
94+ if ( ! match ) {
95+ const materialByName = materials . find ( ( m ) => m . name === initialValue ) ;
96+ if ( materialByName ) match = materialOptions . find ( ( o ) => o . id === materialByName . materialId ) ?? null ;
97+ }
98+
99+ if ( match && match . id !== materialSelected ?. id ) {
100+ setMaterialSelected ( match ) ;
101+ // Update the form value to the formatted label
102+ const fullMaterial = materials . find ( ( m ) => m . materialId === match ! . id ) ?? null ;
103+ onSelect ( fullMaterial ) ;
104+ }
105+ // eslint-disable-next-line react-hooks/exhaustive-deps
106+ } , [ materials , initialValue ] ) ;
107+
108+ if ( isLoading || ! materials ) {
109+ return < LoadingIndicator /> ;
110+ }
111+
112+ if ( isError ) {
113+ return (
114+ < TextField
115+ variant = "outlined"
116+ placeholder = "Select Material"
117+ fullWidth
118+ size = "small"
119+ error
120+ disabled
121+ helperText = { error ?. message || 'Failed to load materials' }
122+ />
123+ ) ;
124+ }
125+
126+ return (
127+ < Autocomplete
128+ sx = { { flex : 1 } }
129+ options = { materialOptions }
130+ getOptionLabel = { ( option ) => option . label }
131+ isOptionEqualToValue = { ( option , value ) => option . id === value . id }
132+ onChange = { ( _ , value ) => {
133+ setMaterialSelected ( value ) ;
134+ const selectedMaterial = value ? ( materials . find ( ( m ) => m . materialId === value . id ) ?? null ) : null ;
135+ onSelect ( selectedMaterial ) ;
136+ } }
137+ value = { materialSelected }
138+ blurOnSelect = { true }
139+ size = { 'small' }
140+ renderInput = { ( params ) => < TextField { ...params } variant = "outlined" placeholder = "Select Material" fullWidth /> }
141+ />
142+ ) ;
143+ } ;
144+
69145const ReimbursementProductTable : React . FC < ReimbursementProductTableProps > = ( {
70146 reimbursementProducts,
71147 removeProduct,
@@ -96,6 +172,7 @@ const ReimbursementProductTable: React.FC<ReimbursementProductTableProps> = ({
96172 const [ showCreateMaterialModal , setShowCreateMaterialModal ] = useState ( false ) ;
97173 const [ currentProductIndex , setCurrentProductIndex ] = useState < number | null > ( null ) ;
98174 const [ currentProject , setCurrentProject ] = useState < ProjectPreview | null > ( null ) ;
175+ const [ pendingMaterialIndices , setPendingMaterialIndices ] = useState < Set < number > > ( new Set ( ) ) ;
99176
100177 const {
101178 data : assemblies ,
@@ -180,6 +257,7 @@ const ReimbursementProductTable: React.FC<ReimbursementProductTableProps> = ({
180257 const handleMaterialCreated = ( materialName : string ) => {
181258 if ( currentProductIndex !== null ) {
182259 setValue ( `reimbursementProducts.${ currentProductIndex } .name` , materialName ) ;
260+ setPendingMaterialIndices ( ( prev ) => new Set ( prev ) . add ( currentProductIndex ) ) ;
183261 }
184262 handleCloseCreateMaterial ( ) ;
185263 } ;
@@ -245,6 +323,7 @@ const ReimbursementProductTable: React.FC<ReimbursementProductTableProps> = ({
245323 setShowSecondSourceFields ( false ) ;
246324 }
247325 } , [ secondRefundSourceName ] ) ;
326+
248327 const {
249328 data : otherReasons ,
250329 isLoading : otherReasonsIsLoading ,
@@ -485,66 +564,97 @@ const ReimbursementProductTable: React.FC<ReimbursementProductTableProps> = ({
485564 width : { xs : '100%' , md : 'auto' }
486565 } }
487566 >
488- < FormControl fullWidth margin = "dense" variant = "outlined" size = "small" >
489- { hasWbsNum ? (
490- // Project-based product: Show field + "Create New Material" button
491- < Box sx = { { display : 'flex' , gap : 1 , alignItems : 'center' } } >
492- < Box sx = { { flex : 1 } } >
493- < Controller
494- name = { `reimbursementProducts.${ product . index } .name` }
495- control = { control }
496- render = { ( { field } ) => (
497- < TextField
498- { ...field }
499- variant = "outlined"
500- placeholder = { 'Product Name/Description' }
501- autoComplete = "off"
502- fullWidth
503- error = { ! ! errors . reimbursementProducts ?. [ product . index ] ?. name }
504- />
505- ) }
506- />
507- < FormHelperText error >
508- { errors . reimbursementProducts ?. [ product . index ] ?. name ?. message }
509- </ FormHelperText >
567+ { hasWbsNum ? (
568+ < FormControl fullWidth margin = "dense" variant = "outlined" size = "small" >
569+ { watch ( `reimbursementProducts.${ product . index } .materialId` ) ||
570+ ! watch ( `reimbursementProducts.${ product . index } .name` ) ||
571+ pendingMaterialIndices . has ( product . index ) ? (
572+ < Box sx = { { display : 'flex' , gap : 1 , alignItems : 'center' } } >
573+ < Box sx = { { flex : 1 } } >
574+ < MaterialAutocomplete
575+ wbsNum = { product . reason as WbsNumber }
576+ initialValue = { watch ( `reimbursementProducts.${ product . index } .name` ) }
577+ onSelect = { ( material ) => {
578+ if ( material ) {
579+ const label = `${ material . name } (${ material . materialTypeName } ): ${ material . manufacturerName ?? 'N/A' } , ${ material . manufacturerPartNumber ?? 'N/A' } ` ;
580+ setValue ( `reimbursementProducts.${ product . index } .name` , label , {
581+ shouldValidate : true ,
582+ shouldDirty : true
583+ } ) ;
584+ setValue (
585+ `reimbursementProducts.${ product . index } .materialId` ,
586+ material . materialId ,
587+ {
588+ shouldValidate : true ,
589+ shouldDirty : true
590+ }
591+ ) ;
592+ setPendingMaterialIndices ( ( prev ) => {
593+ const next = new Set ( prev ) ;
594+ next . delete ( product . index ) ;
595+ return next ;
596+ } ) ;
597+ } else {
598+ setValue ( `reimbursementProducts.${ product . index } .name` , '' , {
599+ shouldValidate : true ,
600+ shouldDirty : true
601+ } ) ;
602+ setValue ( `reimbursementProducts.${ product . index } .materialId` , undefined , {
603+ shouldValidate : true ,
604+ shouldDirty : true
605+ } ) ;
606+ }
607+ } }
608+ />
609+ </ Box >
610+ < Typography fontWeight = "bold" sx = { { whiteSpace : 'nowrap' } } >
611+ OR
612+ </ Typography >
613+ < Button
614+ variant = "outlined"
615+ size = "small"
616+ onClick = { ( ) =>
617+ handleOpenCreateMaterial ( product . index , product . reason as WbsNumber )
618+ }
619+ sx = { { whiteSpace : 'nowrap' } }
620+ >
621+ Create New Material
622+ </ Button >
510623 </ Box >
511- < Typography fontWeight = "bold" sx = { { whiteSpace : 'nowrap' , lineHeight : 1 } } >
512- OR
513- </ Typography >
514- < Button
624+ ) : (
625+ < TextField
515626 variant = "outlined"
627+ value = { watch ( `reimbursementProducts.${ product . index } .name` ) }
628+ fullWidth
516629 size = "small"
517- onClick = { ( ) =>
518- handleOpenCreateMaterial ( product . index , product . reason as WbsNumber )
519- }
520- sx = { { whiteSpace : 'nowrap' } }
521- >
522- Create New Material
523- </ Button >
524- </ Box >
525- ) : (
526- // Other Category: Show only text field
527- < >
528- < Controller
529- name = { `reimbursementProducts.${ product . index } .name` }
530- control = { control }
531- render = { ( { field } ) => (
532- < TextField
533- { ...field }
534- variant = "outlined"
535- placeholder = { 'Product Name/Description' }
536- autoComplete = "off"
537- fullWidth
538- error = { ! ! errors . reimbursementProducts ?. [ product . index ] ?. name }
539- />
540- ) }
630+ disabled
541631 />
542- < FormHelperText error >
543- { errors . reimbursementProducts ?. [ product . index ] ?. name ?. message }
544- </ FormHelperText >
545- </ >
546- ) }
547- </ FormControl >
632+ ) }
633+ < FormHelperText error >
634+ { errors . reimbursementProducts ?. [ product . index ] ?. name ?. message }
635+ </ FormHelperText >
636+ </ FormControl >
637+ ) : (
638+ < FormControl fullWidth margin = "dense" variant = "outlined" size = "small" >
639+ < Controller
640+ name = { `reimbursementProducts.${ product . index } .name` }
641+ control = { control }
642+ render = { ( { field } ) => (
643+ < TextField
644+ { ...field }
645+ variant = "outlined"
646+ placeholder = { 'Product Name/Description' }
647+ autoComplete = "off"
648+ fullWidth
649+ error = { ! ! errors . reimbursementProducts ?. [ product . index ] ?. name }
650+ />
651+ ) }
652+ />
653+ < FormHelperText error >
654+ { errors . reimbursementProducts ?. [ product . index ] ?. name ?. message }
655+ </ FormHelperText >
656+ </ FormControl >
657+ ) }
548658 </ Box >
549659 { ! hasMultipleRefundSources && (
550660 < Box
0 commit comments