Skip to content

Commit e018903

Browse files
Merge pull request #5010 from OneCommunityGlobal/adithya_financials_filter_enhancement
Adithya_Financials_Filter_Enhancement
2 parents faa4657 + 38a6a6f commit e018903

4 files changed

Lines changed: 239 additions & 130 deletions

File tree

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

Lines changed: 144 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,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
);

src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function ExpenditureChart({ projectId }) {
5151
setLoading(true);
5252
setError(null);
5353
try {
54-
// Using mock data instead of API call
54+
// Using mock data
5555
const data = getProjectExpenditure(projectId);
5656
setActual(data.actual);
5757
setPlanned(data.planned);

0 commit comments

Comments
 (0)