Skip to content

Commit d951290

Browse files
committed
feat: added loading state and summary visual cues
1 parent ce90ecb commit d951290

4 files changed

Lines changed: 286 additions & 199 deletions

File tree

src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx

Lines changed: 138 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
CartesianGrid,
1313
LabelList,
1414
} from 'recharts';
15+
import { Spinner } from 'reactstrap';
1516
import { fetchBMProjects } from '../../../../actions/bmdashboard/projectActions';
1617
import { ENDPOINTS } from '../../../../utils/URL';
1718
import 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

Comments
 (0)