@@ -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,175 @@ function ActualVsPlannedCost() {
2122 const projects = useSelector ( state => state . bmProjects ) || [ ] ;
2223 const darkMode = useSelector ( state => state . theme . darkMode ) ;
2324
24- const [ selectedProject , setSelectedProject ] = useState ( '' ) ;
25+ const [ selectedProject , setSelectedProject ] = useState (
26+ ( ) => localStorage . getItem ( 'bm_avsp_project' ) || '' ,
27+ ) ;
28+ const [ selectedCategory , setSelectedCategory ] = useState (
29+ ( ) => localStorage . getItem ( 'bm_avsp_category' ) || 'Overall' ,
30+ ) ;
31+
2532 const [ breakdown , setBreakdown ] = useState ( [ ] ) ;
2633 const [ totals , setTotals ] = useState ( { actual : 0 , planned : 0 } ) ;
34+
2735 const [ loading , setLoading ] = useState ( false ) ;
28- const [ selectedCategory , setSelectedCategory ] = useState ( 'Overall' ) ;
36+ const [ isFiltering , setIsFiltering ] = useState ( false ) ;
2937
3038 const selectedProjectName = useMemo (
3139 ( ) => projects . find ( p => p . _id === selectedProject ) ?. name ?? '' ,
3240 [ projects , selectedProject ] ,
3341 ) ;
3442
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- } ;
43+ useEffect ( ( ) => {
44+ if ( selectedProject ) {
45+ localStorage . setItem ( 'bm_avsp_project' , selectedProject ) ;
46+ }
47+ localStorage . setItem ( 'bm_avsp_category' , selectedCategory ) ;
48+ } , [ selectedProject , selectedCategory ] ) ;
5849
5950 useEffect ( ( ) => {
6051 dispatch ( fetchBMProjects ( ) ) ;
6152 } , [ dispatch ] ) ;
6253
6354 useEffect ( ( ) => {
64- if ( ! selectedProject && projects . length ) {
65- const firstId = projects [ 0 ] . _id ;
66- setSelectedProject ( firstId ) ;
67- fetchExpenses ( firstId ) ;
55+ if ( ! selectedProject && projects . length > 0 ) {
56+ setSelectedProject ( projects [ 0 ] . _id ) ;
6857 }
6958 } , [ projects , selectedProject ] ) ;
7059
60+ useEffect ( ( ) => {
61+ setIsFiltering ( true ) ;
62+ const timeout = setTimeout ( ( ) => {
63+ setIsFiltering ( false ) ;
64+ } , 400 ) ;
65+ return ( ) => clearTimeout ( timeout ) ;
66+ } , [ selectedProject , selectedCategory ] ) ;
67+
68+ useEffect ( ( ) => {
69+ if ( selectedProject ) {
70+ setLoading ( true ) ;
71+ axios
72+ . get ( ENDPOINTS . BM_PROJECT_EXPENSE_BY_ID ( selectedProject ) )
73+ . then ( ( { data } ) => {
74+ setTotals ( {
75+ actual : Math . round ( data . totalActualCost ) ,
76+ planned : Math . round ( data . totalPlannedCost ) ,
77+ } ) ;
78+ setBreakdown (
79+ data . breakdown . map ( item => ( {
80+ category : item . category ,
81+ actualCost : Math . round ( item . actualCost ) ,
82+ plannedCost : Math . round ( item . plannedCost ) ,
83+ } ) ) ,
84+ ) ;
85+ } )
86+ . catch ( ( ) => {
87+ setTotals ( { actual : 0 , planned : 0 } ) ;
88+ setBreakdown ( [ ] ) ;
89+ } )
90+ . finally ( ( ) => setLoading ( false ) ) ;
91+ }
92+ } , [ selectedProject ] ) ;
93+
7194 const categories = [ 'Overall' , ...new Set ( breakdown . map ( d => d . category ) ) ] ;
7295 const chartData =
7396 selectedCategory === 'Overall'
7497 ? [ { category : 'Overall' , actualCost : totals . actual , plannedCost : totals . planned } ]
7598 : breakdown . filter ( d => d . category === selectedCategory ) ;
7699
77- // ---- Extracted chart content ----
100+ const filterSummary = `${ selectedProjectName || 'Loading...' } - ${ selectedCategory } ` ;
101+
78102 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 > ;
103+ if ( loading || isFiltering ) {
104+ chartContent = (
105+ < div
106+ style = { {
107+ display : 'flex' ,
108+ height : 200 ,
109+ justifyContent : 'center' ,
110+ alignItems : 'center' ,
111+ color : 'var(--text-color)' ,
112+ } }
113+ >
114+ < Spinner color = "primary" size = "sm" />
115+ < span style = { { marginLeft : '10px' } } > Updating chart...</ span >
116+ </ div >
117+ ) ;
118+ } else if (
119+ ! chartData . length ||
120+ ( chartData . length === 1 && chartData [ 0 ] . actualCost === 0 && chartData [ 0 ] . plannedCost === 0 )
121+ ) {
122+ chartContent = (
123+ < div
124+ style = { {
125+ display : 'flex' ,
126+ height : 200 ,
127+ justifyContent : 'center' ,
128+ alignItems : 'center' ,
129+ color : 'var(--text-color)' ,
130+ fontStyle : 'italic' ,
131+ } }
132+ >
133+ No data available for the selected filters.
134+ </ div >
135+ ) ;
83136 } else {
84137 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 }
138+ < div style = { { width : '100%' , height : 200 } } >
139+ < ResponsiveContainer width = "100%" height = "100%" >
140+ < BarChart data = { chartData } margin = { { top : 20 , right : 5 , left : 5 , bottom : 0 } } barGap = { 20 } >
141+ < CartesianGrid strokeDasharray = "3 3" />
142+ < XAxis
143+ dataKey = "category"
144+ axisLine = { false }
145+ tickLine = { false }
146+ tick = { { fill : 'var(--text-color)' } }
147+ />
148+ < YAxis tick = { { fill : 'var(--text-color)' , fontSize : '12px' } } />
149+ < Tooltip
150+ contentStyle = { {
151+ backgroundColor : 'var(--card-bg)' ,
152+ borderColor : 'var(--button-hover)' ,
153+ } }
154+ labelStyle = { { color : 'var(--text-color)' , fontSize : '12px' } }
155+ />
156+ < Legend
157+ verticalAlign = "top"
158+ height = { 36 }
159+ iconSize = { 8 }
160+ wrapperStyle = { { color : 'var(--text-color)' } }
161+ />
162+ < Bar
163+ dataKey = "actualCost"
164+ name = "Actual"
165+ fill = { darkMode ? '#c0392b' : '#e74a3b' }
166+ barSize = { 40 }
92167 >
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- </ >
168+ < LabelList dataKey = "actualCost" position = "top" fill = "var(--text-color)" />
169+ </ Bar >
170+ < Bar
171+ dataKey = "plannedCost"
172+ name = "Planned"
173+ fill = { ! darkMode ? '#17a272' : '#1cc88a' }
174+ barSize = { 40 }
175+ >
176+ < LabelList dataKey = "plannedCost" position = "top" fill = "var(--text-color)" />
177+ </ Bar >
178+ </ BarChart >
179+ </ ResponsiveContainer >
180+ </ div >
135181 ) ;
136182 }
137183
138184 return (
139185 < div style = { { padding : 10 } } >
140- < h2 style = { { fontSize : 'large' , marginBottom : '3px' } } className = { styles . title } >
141- Actual vs Planned Costs
142- </ h2 >
186+ < div style = { { textAlign : 'center' , marginBottom : '15px' } } >
187+ < h2 style = { { fontSize : 'large' , margin : '0 0 5px 0' } } className = { styles . title } >
188+ Actual vs Planned Costs
189+ </ h2 >
190+ < div style = { { fontSize : '0.85rem' , color : 'var(--text-color)' , fontWeight : 'bold' } } >
191+ Viewing: { filterSummary }
192+ </ div >
193+ </ div >
143194
144195 < div className = { styles . selectorsContainer } >
145196 < div className = { styles . selectorGroup } >
@@ -148,9 +199,7 @@ function ActualVsPlannedCost() {
148199 id = "ActualVsPlannedCost-project-select"
149200 value = { selectedProject }
150201 onChange = { e => {
151- const id = e . target . value ;
152- setSelectedProject ( id ) ;
153- fetchExpenses ( id ) ;
202+ setSelectedProject ( e . target . value ) ;
154203 setSelectedCategory ( 'Overall' ) ;
155204 } }
156205 >
@@ -178,7 +227,6 @@ function ActualVsPlannedCost() {
178227 </ div >
179228 </ div >
180229
181- { /* Render chart/loading/no-data */ }
182230 { chartContent }
183231 </ div >
184232 ) ;
0 commit comments