1- import { useState } from 'react' ;
1+ import { useEffect , useState } from 'react' ;
22import { useSelector } from 'react-redux' ;
33import { useHistory } from 'react-router-dom' ;
44import { toast } from 'react-toastify' ;
@@ -10,15 +10,94 @@ import { purchaseTools } from '~/actions/bmdashboard/toolActions';
1010
1111import styles from './PurchaseForm.module.css' ;
1212
13+ const PRIORITY_ORDER = { Low : 1 , Medium : 2 , High : 3 } ;
14+ const RECENT_REQUEST_WINDOW_DAYS = 30 ;
15+
16+ function getObjectId ( value ) {
17+ if ( ! value ) return '' ;
18+ if ( typeof value === 'string' ) return value ;
19+ if ( typeof value === 'object' && value . _id ) return value . _id ;
20+ return '' ;
21+ }
22+
23+ function formatDate ( dateValue ) {
24+ if ( ! dateValue ) return 'No prior requests' ;
25+
26+ const date = new Date ( dateValue ) ;
27+
28+ if ( Number . isNaN ( date . getTime ( ) ) ) {
29+ return 'No prior requests' ;
30+ }
31+
32+ return date . toLocaleDateString ( 'en-US' , {
33+ month : 'short' ,
34+ day : 'numeric' ,
35+ year : 'numeric' ,
36+ } ) ;
37+ }
38+
39+ function getDaysSince ( dateValue ) {
40+ if ( ! dateValue ) return Number . POSITIVE_INFINITY ;
41+
42+ const date = new Date ( dateValue ) ;
43+
44+ if ( Number . isNaN ( date . getTime ( ) ) ) {
45+ return Number . POSITIVE_INFINITY ;
46+ }
47+
48+ return Math . floor ( ( Date . now ( ) - date . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) ) ;
49+ }
50+
51+ function getSuggestedPriority ( records , availabilitySummary ) {
52+ if ( records . length ) {
53+ const counts = records . reduce ( ( acc , record ) => {
54+ const recordPriority = record . priority || 'Low' ;
55+ const current = acc [ recordPriority ] || { count : 0 , latest : 0 } ;
56+ const latest = new Date ( record . date || 0 ) . getTime ( ) ;
57+
58+ acc [ recordPriority ] = {
59+ count : current . count + 1 ,
60+ latest : Math . max ( current . latest , Number . isNaN ( latest ) ? 0 : latest ) ,
61+ } ;
62+
63+ return acc ;
64+ } , { } ) ;
65+
66+ return Object . entries ( counts ) . sort ( ( a , b ) => {
67+ if ( b [ 1 ] . count !== a [ 1 ] . count ) return b [ 1 ] . count - a [ 1 ] . count ;
68+ if ( b [ 1 ] . latest !== a [ 1 ] . latest ) return b [ 1 ] . latest - a [ 1 ] . latest ;
69+ return PRIORITY_ORDER [ b [ 0 ] ] - PRIORITY_ORDER [ a [ 0 ] ] ;
70+ } ) [ 0 ] [ 0 ] ;
71+ }
72+
73+ if (
74+ availabilitySummary . projectAvailableCount === 0 &&
75+ availabilitySummary . projectUsingCount > 0
76+ ) {
77+ return 'High' ;
78+ }
79+
80+ if (
81+ availabilitySummary . projectAvailableCount === 0 &&
82+ availabilitySummary . globalAvailableCount > 0
83+ ) {
84+ return 'Medium' ;
85+ }
86+
87+ return 'Low' ;
88+ }
89+
1390export default function PurchaseForm ( ) {
1491 const bmProjects = useSelector ( state => state . bmProjects ) ;
1592 const tools = useSelector ( state => state . bmInvTypes . list ) ;
93+ const toolInventory = useSelector ( state => state . bmTools . toolslist || [ ] ) ;
1694 const history = useHistory ( ) ;
1795
1896 const [ projectId , setProjectId ] = useState ( '' ) ;
1997 const [ toolId , setToolId ] = useState ( '' ) ;
2098 const [ quantity , setQty ] = useState ( '' ) ;
2199 const [ priority , setPriority ] = useState ( 'Low' ) ;
100+ const [ priorityTouched , setPriorityTouched ] = useState ( false ) ;
22101 const [ makeModel , setMakeModel ] = useState ( '' ) ;
23102 const [ estTime , setEstTime ] = useState ( '' ) ;
24103 const [ desc , setDesc ] = useState ( '' ) ;
@@ -40,6 +119,105 @@ export default function PurchaseForm() {
40119 makeModel : Joi . string ( ) . allow ( '' ) ,
41120 } ) ;
42121
122+ const sortedTools = [ ...tools ] . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
123+ const selectedTool = sortedTools . find ( tool => tool . _id === toolId ) ;
124+
125+ const selectedProjectInventory = projectId
126+ ? toolInventory . filter ( tool => getObjectId ( tool . project ) === projectId )
127+ : [ ] ;
128+
129+ const projectSpecificToolIds = new Set ( ) ;
130+
131+ selectedProjectInventory . forEach ( tool => {
132+ const itemTypeId = getObjectId ( tool . itemType ) ;
133+
134+ if ( itemTypeId ) {
135+ projectSpecificToolIds . add ( itemTypeId ) ;
136+ }
137+ } ) ;
138+
139+ sortedTools . forEach ( tool => {
140+ const availableOnProject = ( tool . available || [ ] ) . some (
141+ item => getObjectId ( item . project ) === projectId ,
142+ ) ;
143+ const inUseOnProject = ( tool . using || [ ] ) . some ( item => getObjectId ( item . project ) === projectId ) ;
144+
145+ if ( availableOnProject || inUseOnProject ) {
146+ projectSpecificToolIds . add ( tool . _id ) ;
147+ }
148+ } ) ;
149+
150+ const projectFocusedTools = sortedTools . filter ( tool => projectSpecificToolIds . has ( tool . _id ) ) ;
151+ const otherTools = sortedTools . filter ( tool => ! projectSpecificToolIds . has ( tool . _id ) ) ;
152+ const selectedProjectName = bmProjects . find ( project => project . _id === projectId ) ?. name || '' ;
153+
154+ const matchingToolRecords = toolId
155+ ? toolInventory . filter ( tool => getObjectId ( tool . itemType ) === toolId )
156+ : [ ] ;
157+ const matchingProjectToolRecords = matchingToolRecords . filter (
158+ tool => getObjectId ( tool . project ) === projectId ,
159+ ) ;
160+
161+ const projectPurchaseHistory = matchingProjectToolRecords
162+ . flatMap ( tool => tool . purchaseRecord || [ ] )
163+ . sort ( ( a , b ) => new Date ( b . date || 0 ) - new Date ( a . date || 0 ) ) ;
164+
165+ const crossProjectPurchaseHistory = matchingToolRecords
166+ . flatMap ( tool => tool . purchaseRecord || [ ] )
167+ . sort ( ( a , b ) => new Date ( b . date || 0 ) - new Date ( a . date || 0 ) ) ;
168+
169+ const projectAvailableCount = ( selectedTool ?. available || [ ] ) . filter (
170+ item => getObjectId ( item . project ) === projectId ,
171+ ) . length ;
172+ const projectUsingCount = ( selectedTool ?. using || [ ] ) . filter (
173+ item => getObjectId ( item . project ) === projectId ,
174+ ) . length ;
175+ const globalAvailableCount = selectedTool ?. available ?. length || 0 ;
176+ const globalUsingCount = selectedTool ?. using ?. length || 0 ;
177+
178+ const availabilitySummary = {
179+ projectAvailableCount,
180+ projectUsingCount,
181+ globalAvailableCount,
182+ globalUsingCount,
183+ } ;
184+
185+ const suggestedPriority = getSuggestedPriority ( projectPurchaseHistory , availabilitySummary ) ;
186+ const lastRequestedRecord = projectPurchaseHistory [ 0 ] || crossProjectPurchaseHistory [ 0 ] || null ;
187+ const recentDuplicateRecord =
188+ projectPurchaseHistory . find (
189+ record => getDaysSince ( record . date ) <= RECENT_REQUEST_WINDOW_DAYS ,
190+ ) || null ;
191+
192+ const availabilityStatus = ( ( ) => {
193+ if ( ! selectedTool ) return 'Select a tool to view availability.' ;
194+ if ( ! projectId )
195+ return `${ globalAvailableCount } available and ${ globalUsingCount } in use across all projects.` ;
196+ if ( projectAvailableCount > 0 ) {
197+ return `${ projectAvailableCount } available on ${ selectedProjectName } .` ;
198+ }
199+ if ( projectUsingCount > 0 ) {
200+ return `${ projectUsingCount } currently in use on ${ selectedProjectName } .` ;
201+ }
202+ if ( globalAvailableCount > 0 ) {
203+ return `${ globalAvailableCount } available on other projects.` ;
204+ }
205+ if ( globalUsingCount > 0 ) {
206+ return `${ globalUsingCount } currently in use across other projects.` ;
207+ }
208+ return 'No active inventory found for this tool yet.' ;
209+ } ) ( ) ;
210+
211+ useEffect ( ( ) => {
212+ if ( ! priorityTouched ) {
213+ setPriority ( suggestedPriority ) ;
214+ }
215+ } , [ suggestedPriority , priorityTouched ] ) ;
216+
217+ useEffect ( ( ) => {
218+ setPriorityTouched ( false ) ;
219+ } , [ projectId , toolId ] ) ;
220+
43221 const handleSubmit = async e => {
44222 e . preventDefault ( ) ;
45223 const { error } = schema . validate ( {
@@ -73,6 +251,7 @@ export default function PurchaseForm() {
73251 setToolId ( '' ) ;
74252 setQty ( '' ) ;
75253 setPriority ( 'Low' ) ;
254+ setPriorityTouched ( false ) ;
76255 setMakeModel ( '' ) ;
77256 setEstTime ( '' ) ;
78257 setDesc ( '' ) ;
@@ -128,13 +307,70 @@ export default function PurchaseForm() {
128307 < option disabled hidden value = "" >
129308 { ' ' }
130309 </ option >
131- { tools . map ( ( { _id, name } ) => (
132- < option value = { _id } key = { _id } >
133- { name }
134- </ option >
135- ) ) }
310+ { ! projectId &&
311+ sortedTools . map ( ( { _id, name } ) => (
312+ < option value = { _id } key = { _id } >
313+ { name }
314+ </ option >
315+ ) ) }
316+ { ! ! projectId && ! ! projectFocusedTools . length && (
317+ < optgroup label = { `Suggested for ${ selectedProjectName } ` } >
318+ { projectFocusedTools . map ( ( { _id, name } ) => (
319+ < option value = { _id } key = { _id } >
320+ { name }
321+ </ option >
322+ ) ) }
323+ </ optgroup >
324+ ) }
325+ { ! ! projectId && ! ! otherTools . length && (
326+ < optgroup label = { projectFocusedTools . length ? 'Other Tool Types' : 'All Tool Types' } >
327+ { otherTools . map ( ( { _id, name } ) => (
328+ < option value = { _id } key = { _id } >
329+ { name }
330+ </ option >
331+ ) ) }
332+ </ optgroup >
333+ ) }
136334 </ Input >
335+ { projectId && (
336+ < FormText >
337+ { projectFocusedTools . length
338+ ? 'Tools already used, available, or previously requested on this project are grouped first.'
339+ : 'No prior tool history was found for this project, so the full catalog is shown.' }
340+ </ FormText >
341+ ) }
137342 </ FormGroup >
343+ { toolId && (
344+ < section className = { styles . toolInsightPanel } >
345+ < div className = { styles . toolInsightHeader } >
346+ < h3 > Selection Insights</ h3 >
347+ < span className = { styles . toolInsightPriority } >
348+ Suggested priority: { suggestedPriority }
349+ </ span >
350+ </ div >
351+ < dl className = { styles . toolInsightGrid } >
352+ < div >
353+ < dt > Availability Status</ dt >
354+ < dd > { availabilityStatus } </ dd >
355+ </ div >
356+ < div >
357+ < dt > Last Requested</ dt >
358+ < dd > { formatDate ( lastRequestedRecord ?. date ) } </ dd >
359+ </ div >
360+ < div >
361+ < dt > Common Use Case</ dt >
362+ < dd > { selectedTool ?. description || 'No usage guidance added for this tool yet.' } </ dd >
363+ </ div >
364+ </ dl >
365+ { recentDuplicateRecord && selectedProjectName && (
366+ < p className = { styles . toolWarning } >
367+ Warning: this tool was already requested for { selectedProjectName } on{ ' ' }
368+ { formatDate ( recentDuplicateRecord . date ) } with { recentDuplicateRecord . priority } { ' ' }
369+ priority.
370+ </ p >
371+ ) }
372+ </ section >
373+ ) }
138374 < div className = { `${ styles . purchaseToolFlexGroup } ` } >
139375 < FormGroup className = { `${ styles . flexGroupQty } ` } >
140376 < Label for = "input-quantity" > Quantity</ Label >
@@ -159,6 +395,7 @@ export default function PurchaseForm() {
159395 value = { priority }
160396 onChange = { ( { currentTarget } ) => {
161397 setValidationError ( '' ) ;
398+ setPriorityTouched ( true ) ;
162399 setPriority ( currentTarget . value ) ;
163400 } }
164401 >
@@ -210,6 +447,20 @@ export default function PurchaseForm() {
210447 < div className = { `${ styles . purchaseToolError } ` } >
211448 { validationError && < p > { validationError } </ p > }
212449 </ div >
450+ { ! ! projectId && ! ! toolId && (
451+ < div className = { styles . confirmationSummary } >
452+ < h3 > Ready to Submit</ h3 >
453+ < p >
454+ You are requesting { quantity || '0' } { selectedTool ?. name || 'tool' } for{ ' ' }
455+ { selectedProjectName || 'the selected project' } with { priority } priority.
456+ </ p >
457+ < p >
458+ Expected use: { estTime || 'not provided yet' } .
459+ { makeModel ? ` Preferred make/model: ${ makeModel } .` : '' }
460+ </ p >
461+ < p > { desc || 'Add a short usage description before submitting.' } </ p >
462+ </ div >
463+ ) }
213464 < div className = { `${ styles . purchaseToolButtons } ` } >
214465 < Button
215466 type = "button"
0 commit comments