@@ -12,6 +12,7 @@ import {
1212 CartesianGrid ,
1313 LabelList ,
1414} from 'recharts' ;
15+ import { Spinner } from 'reactstrap' ;
1516import { fetchBMProjects } from '../../../../actions/bmdashboard/projectActions' ;
1617import { ENDPOINTS } from '../../../../utils/URL' ;
1718import styles from './ActualVsPlannedCost.module.css' ;
@@ -21,125 +22,181 @@ function ActualVsPlannedCost() {
2122 const projects = useSelector ( state => state . bmProjects ) || [ ] ;
2223 const darkMode = useSelector ( state => state . theme . darkMode ) ;
2324
24- const [ selectedProject , setSelectedProject ] = useState ( '' ) ;
25+ // Persisted filters
26+ const [ selectedProject , setSelectedProject ] = useState (
27+ ( ) => localStorage . getItem ( 'bm_avsp_project' ) || '' ,
28+ ) ;
29+ const [ selectedCategory , setSelectedCategory ] = useState (
30+ ( ) => localStorage . getItem ( 'bm_avsp_category' ) || 'Overall' ,
31+ ) ;
32+
33+ // Component state
2534 const [ breakdown , setBreakdown ] = useState ( [ ] ) ;
2635 const [ totals , setTotals ] = useState ( { actual : 0 , planned : 0 } ) ;
2736 const [ loading , setLoading ] = useState ( false ) ;
28- const [ selectedCategory , setSelectedCategory ] = useState ( 'Overall' ) ;
37+ const [ isFiltering , setIsFiltering ] = useState ( false ) ;
2938
3039 const selectedProjectName = useMemo (
3140 ( ) => projects . find ( p => p . _id === selectedProject ) ?. name ?? '' ,
3241 [ projects , selectedProject ] ,
3342 ) ;
3443
35- const fetchExpenses = projectId => {
36- setLoading ( true ) ;
37- axios
38- . get ( ENDPOINTS . BM_PROJECT_EXPENSE_BY_ID ( projectId ) )
39- . then ( ( { data } ) => {
40- setTotals ( {
41- actual : Math . round ( data . totalActualCost ) ,
42- planned : Math . round ( data . totalPlannedCost ) ,
43- } ) ;
44- setBreakdown (
45- data . breakdown . map ( item => ( {
46- category : item . category ,
47- actualCost : Math . round ( item . actualCost ) ,
48- plannedCost : Math . round ( item . plannedCost ) ,
49- } ) ) ,
50- ) ;
51- } )
52- . catch ( ( ) => {
53- setTotals ( { actual : 0 , planned : 0 } ) ;
54- setBreakdown ( [ ] ) ;
55- } )
56- . finally ( ( ) => setLoading ( false ) ) ;
57- } ;
44+ // Sync filters to local storage
45+ useEffect ( ( ) => {
46+ if ( selectedProject ) {
47+ localStorage . setItem ( 'bm_avsp_project' , selectedProject ) ;
48+ }
49+ localStorage . setItem ( 'bm_avsp_category' , selectedCategory ) ;
50+ } , [ selectedProject , selectedCategory ] ) ;
5851
5952 useEffect ( ( ) => {
6053 dispatch ( fetchBMProjects ( ) ) ;
6154 } , [ dispatch ] ) ;
6255
56+ // Default to first project if none selected
6357 useEffect ( ( ) => {
64- if ( ! selectedProject && projects . length ) {
65- const firstId = projects [ 0 ] . _id ;
66- setSelectedProject ( firstId ) ;
67- fetchExpenses ( firstId ) ;
58+ if ( ! selectedProject && projects . length > 0 ) {
59+ setSelectedProject ( projects [ 0 ] . _id ) ;
6860 }
6961 } , [ projects , selectedProject ] ) ;
7062
63+ // Filter transition effect
64+ useEffect ( ( ) => {
65+ setIsFiltering ( true ) ;
66+ const timeout = setTimeout ( ( ) => {
67+ setIsFiltering ( false ) ;
68+ } , 400 ) ;
69+ return ( ) => clearTimeout ( timeout ) ;
70+ } , [ selectedProject , selectedCategory ] ) ;
71+
72+ // Fetch project expenses
73+ useEffect ( ( ) => {
74+ if ( selectedProject ) {
75+ setLoading ( true ) ;
76+ axios
77+ . get ( ENDPOINTS . BM_PROJECT_EXPENSE_BY_ID ( selectedProject ) )
78+ . then ( ( { data } ) => {
79+ setTotals ( {
80+ actual : Math . round ( data . totalActualCost ) ,
81+ planned : Math . round ( data . totalPlannedCost ) ,
82+ } ) ;
83+ setBreakdown (
84+ data . breakdown . map ( item => ( {
85+ category : item . category ,
86+ actualCost : Math . round ( item . actualCost ) ,
87+ plannedCost : Math . round ( item . plannedCost ) ,
88+ } ) ) ,
89+ ) ;
90+ } )
91+ . catch ( ( ) => {
92+ setTotals ( { actual : 0 , planned : 0 } ) ;
93+ setBreakdown ( [ ] ) ;
94+ } )
95+ . finally ( ( ) => setLoading ( false ) ) ;
96+ }
97+ } , [ selectedProject ] ) ;
98+
99+ // Derived chart data
71100 const categories = [ 'Overall' , ...new Set ( breakdown . map ( d => d . category ) ) ] ;
72101 const chartData =
73102 selectedCategory === 'Overall'
74103 ? [ { category : 'Overall' , actualCost : totals . actual , plannedCost : totals . planned } ]
75104 : breakdown . filter ( d => d . category === selectedCategory ) ;
76105
77- // ---- Extracted chart content ----
106+ const filterSummary = `${ selectedProjectName || 'Loading...' } - ${ selectedCategory } ` ;
107+
78108 let chartContent ;
79- if ( loading ) {
80- chartContent = < p > Loading data…</ p > ;
81- } else if ( ! chartData . length ) {
82- chartContent = < p > No data available for this category.</ p > ;
109+ if ( loading || isFiltering ) {
110+ chartContent = (
111+ < div
112+ style = { {
113+ display : 'flex' ,
114+ height : 200 ,
115+ justifyContent : 'center' ,
116+ alignItems : 'center' ,
117+ color : 'var(--text-color)' ,
118+ } }
119+ >
120+ < Spinner color = "primary" size = "sm" />
121+ < span style = { { marginLeft : '10px' } } > Updating chart...</ span >
122+ </ div >
123+ ) ;
124+ } else if (
125+ ! chartData . length ||
126+ ( chartData . length === 1 && chartData [ 0 ] . actualCost === 0 && chartData [ 0 ] . plannedCost === 0 )
127+ ) {
128+ chartContent = (
129+ < div
130+ style = { {
131+ display : 'flex' ,
132+ height : 200 ,
133+ justifyContent : 'center' ,
134+ alignItems : 'center' ,
135+ color : 'var(--text-color)' ,
136+ fontStyle : 'italic' ,
137+ } }
138+ >
139+ No data available for the selected filters.
140+ </ div >
141+ ) ;
83142 } else {
84143 chartContent = (
85- < >
86- < div style = { { width : '100%' , height : 200 } } >
87- < ResponsiveContainer width = "100%" height = "100%" >
88- < BarChart
89- data = { chartData }
90- margin = { { top : 5 , right : 5 , left : 5 , bottom : 0 } }
91- barGap = { 20 }
144+ < div style = { { width : '100%' , height : 200 } } >
145+ < ResponsiveContainer width = "100%" height = "100%" >
146+ < BarChart data = { chartData } margin = { { top : 20 , right : 5 , left : 5 , bottom : 0 } } barGap = { 20 } >
147+ < CartesianGrid strokeDasharray = "3 3" />
148+ < XAxis
149+ dataKey = "category"
150+ axisLine = { false }
151+ tickLine = { false }
152+ tick = { { fill : 'var(--text-color)' } }
153+ />
154+ < YAxis tick = { { fill : 'var(--text-color)' , fontSize : '12px' } } />
155+ < Tooltip
156+ contentStyle = { {
157+ backgroundColor : 'var(--card-bg)' ,
158+ borderColor : 'var(--button-hover)' ,
159+ } }
160+ labelStyle = { { color : 'var(--text-color)' , fontSize : '12px' } }
161+ />
162+ < Legend
163+ verticalAlign = "top"
164+ height = { 36 }
165+ iconSize = { 8 }
166+ wrapperStyle = { { color : 'var(--text-color)' } }
167+ />
168+ < Bar
169+ dataKey = "actualCost"
170+ name = "Actual"
171+ fill = { darkMode ? '#c0392b' : '#e74a3b' }
172+ barSize = { 40 }
92173 >
93- < CartesianGrid strokeDasharray = "3 3" />
94- < XAxis
95- dataKey = "category"
96- axisLine = { false }
97- tickLine = { false }
98- tick = { { fill : 'var(--text-color)' } }
99- />
100- < YAxis tick = { { fill : 'var(--text-color)' , fontSize : '12px' } } />
101- < Tooltip
102- contentStyle = { {
103- backgroundColor : 'var(--card-bg)' ,
104- borderColor : 'var(--button-hover)' ,
105- } }
106- labelStyle = { { color : 'var(--text-color)' , fontSize : '12px' } }
107- />
108- < Legend
109- verticalAlign = "top"
110- height = { 36 }
111- iconSize = { 8 }
112- wrapperStyle = { { color : 'var(--text-color)' } }
113- />
114- < Bar
115- dataKey = "actualCost"
116- name = "Actual"
117- fill = { darkMode ? '#c0392b' : '#e74a3b' }
118- barSize = { 40 }
119- >
120- < LabelList dataKey = "actualCost" position = "top" fill = "var(--text-color)" />
121- </ Bar >
122- < Bar
123- dataKey = "plannedCost"
124- name = "Planned"
125- fill = { ! darkMode ? '#17a272' : '#1cc88a' }
126- barSize = { 40 }
127- >
128- < LabelList dataKey = "plannedCost" position = "top" fill = "var(--text-color)" />
129- </ Bar >
130- </ BarChart >
131- </ ResponsiveContainer >
132- </ div >
133- < div className = { styles . chartCaption } > { selectedProjectName } </ div >
134- </ >
174+ < LabelList dataKey = "actualCost" position = "top" fill = "var(--text-color)" />
175+ </ Bar >
176+ < Bar
177+ dataKey = "plannedCost"
178+ name = "Planned"
179+ fill = { ! darkMode ? '#17a272' : '#1cc88a' }
180+ barSize = { 40 }
181+ >
182+ < LabelList dataKey = "plannedCost" position = "top" fill = "var(--text-color)" />
183+ </ Bar >
184+ </ BarChart >
185+ </ ResponsiveContainer >
186+ </ div >
135187 ) ;
136188 }
137189
138190 return (
139191 < div style = { { padding : 10 } } >
140- < h2 style = { { fontSize : 'large' , marginBottom : '3px' } } className = { styles . title } >
141- Actual vs Planned Costs
142- </ h2 >
192+ < div style = { { textAlign : 'center' , marginBottom : '15px' } } >
193+ < h2 style = { { fontSize : 'large' , margin : '0 0 5px 0' } } className = { styles . title } >
194+ Actual vs Planned Costs
195+ </ h2 >
196+ < div style = { { fontSize : '0.85rem' , color : 'var(--text-color)' , fontWeight : 'bold' } } >
197+ Viewing: { filterSummary }
198+ </ div >
199+ </ div >
143200
144201 < div className = { styles . selectorsContainer } >
145202 < div className = { styles . selectorGroup } >
@@ -148,9 +205,7 @@ function ActualVsPlannedCost() {
148205 id = "ActualVsPlannedCost-project-select"
149206 value = { selectedProject }
150207 onChange = { e => {
151- const id = e . target . value ;
152- setSelectedProject ( id ) ;
153- fetchExpenses ( id ) ;
208+ setSelectedProject ( e . target . value ) ;
154209 setSelectedCategory ( 'Overall' ) ;
155210 } }
156211 >
@@ -178,7 +233,6 @@ function ActualVsPlannedCost() {
178233 </ div >
179234 </ div >
180235
181- { /* Render chart/loading/no-data */ }
182236 { chartContent }
183237 </ div >
184238 ) ;
0 commit comments