Skip to content

Commit c0b9c15

Browse files
Merge pull request #4495 from OneCommunityGlobal/sriamsh_project_status_donut_chart_frontend
Sriamsh - Implement “Project Status” Donut Chart
2 parents efbe106 + 1d01292 commit c0b9c15

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

src/components/BMDashboard/BMDashboard.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Container } from 'reactstrap';
44
import { fetchBMProjects } from '../../actions/bmdashboard/projectActions';
55
import ProjectsList from './Projects/ProjectsList';
66
import ProjectSelectForm from './Projects/ProjectSelectForm';
7+
import ProjectStatusDonutChart from './ProjectStatus/ProjectStatusDonutChart';
78
import BMError from './shared/BMError';
89
import styles from './BMDashboard.module.css';
910

@@ -200,6 +201,7 @@ export function BMDashboard() {
200201
) : (
201202
<>
202203
<ProjectSelectForm />
204+
<ProjectStatusDonutChart />
203205
<ProjectsList />
204206
</>
205207
)}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, Label } from 'recharts';
3+
import axios from 'axios';
4+
import styles from './ProjectStatusDonutChart.module.css';
5+
6+
const COLORS = ['#B39DDB', '#80DEEA', '#FFABAB']; // Active, Completed, Delayed
7+
8+
export default function ProjectStatusDonutChart() {
9+
const [loading, setLoading] = useState(true);
10+
const [error, setError] = useState(null);
11+
const [statusData, setStatusData] = useState(null);
12+
13+
const [startDate, setStartDate] = useState('');
14+
const [endDate, setEndDate] = useState('');
15+
16+
const fetchStatus = async () => {
17+
try {
18+
setLoading(true);
19+
setError(null);
20+
21+
// Build query string
22+
const query = [];
23+
if (startDate) query.push(`startDate=${startDate}`);
24+
if (endDate) query.push(`endDate=${endDate}`);
25+
const queryString = query.length ? `?${query.join('&')}` : '';
26+
27+
// Get token from localStorage (Dev Admin session)
28+
const token = localStorage.getItem('token');
29+
30+
const res = await axios.get(`http://localhost:4500/api/projects/status${queryString}`, {
31+
headers: { Authorization: token },
32+
});
33+
34+
// TEMPORARY MOCK DATA - for testing purposes
35+
/*setStatusData({
36+
totalProjects: 50,
37+
activeProjects: 20,
38+
completedProjects: 20,
39+
delayedProjects: 10,
40+
});
41+
return;*/
42+
43+
setStatusData(res.data);
44+
} catch (err) {
45+
// console.error(err);
46+
setError('Unable to load project status.');
47+
} finally {
48+
setLoading(false);
49+
}
50+
};
51+
52+
useEffect(() => {
53+
fetchStatus();
54+
}, []);
55+
56+
if (loading) return <p>Loading project status...</p>;
57+
if (error) return <p>{error}</p>;
58+
if (!statusData) return <p>No data available.</p>;
59+
60+
const pieData = [
61+
{ name: 'Active Projects', value: statusData.activeProjects },
62+
{ name: 'Completed Projects', value: statusData.completedProjects },
63+
{ name: 'Delayed Projects', value: statusData.delayedProjects },
64+
];
65+
66+
// SHOW MESSAGE WHEN THERE IS NO DATA
67+
if (pieData.every(item => item.value === 0)) {
68+
return (
69+
<div className={styles.container}>
70+
<h2 className={styles.title}>PROJECT STATUS</h2>
71+
<p className={styles.noDataMessage}>No project status data available.</p>
72+
</div>
73+
);
74+
}
75+
76+
const today = new Date().toLocaleDateString('en-US', {
77+
weekday: 'short',
78+
month: 'short',
79+
day: 'numeric',
80+
year: 'numeric',
81+
});
82+
83+
const allZero =
84+
!statusData.activeProjects && !statusData.completedProjects && !statusData.delayedProjects;
85+
86+
return (
87+
<div className={styles.container}>
88+
<h2 className={styles.title}>PROJECT STATUS</h2>
89+
90+
<div className={styles.filterRow}>
91+
<input
92+
type="date"
93+
value={startDate}
94+
max={endDate || undefined}
95+
onChange={e => setStartDate(e.target.value)}
96+
/>
97+
98+
<input
99+
type="date"
100+
value={endDate}
101+
min={startDate || undefined}
102+
onChange={e => setEndDate(e.target.value)}
103+
/>
104+
105+
<button type="button" onClick={fetchStatus} className={styles.applyBtn}>
106+
Apply
107+
</button>
108+
</div>
109+
110+
<div className={styles.chartWrapper}>
111+
{/* Only draw the ring if at least one status has data */}
112+
{!allZero && (
113+
<ResponsiveContainer width="100%" aspect={1}>
114+
<PieChart margin={{ top: 10, right: 10, bottom: 40, left: 10 }}>
115+
<Pie
116+
data={pieData}
117+
cx="50%"
118+
cy="50%"
119+
innerRadius="60%"
120+
outerRadius="85%"
121+
paddingAngle={3}
122+
dataKey="value"
123+
>
124+
{pieData.map((entry, index) => (
125+
<Cell key={entry.name} fill={COLORS[index % COLORS.length]} />
126+
))}
127+
<Label
128+
position="center"
129+
content={({ viewBox }) => {
130+
const { cx, cy } = viewBox;
131+
return (
132+
<text x={cx} y={cy} textAnchor="middle" dominantBaseline="central">
133+
<tspan x={cx} dy="-10" className={styles.centerLabel}>
134+
Total Projects
135+
</tspan>
136+
<tspan x={cx} dy="24" className={styles.centerValue}>
137+
{statusData.totalProjects}
138+
</tspan>
139+
</text>
140+
);
141+
}}
142+
/>
143+
</Pie>
144+
145+
<Tooltip />
146+
<Legend verticalAlign="bottom" align="center" />
147+
</PieChart>
148+
</ResponsiveContainer>
149+
)}
150+
151+
<div className={styles.summaryBox}>
152+
<h3>{today}</h3>
153+
154+
<p className={styles.label}>ACTIVE PROJECTS</p>
155+
<span className={styles.value}>{statusData.activeProjects}</span>
156+
157+
<p className={styles.label}>COMPLETED PROJECTS</p>
158+
<span className={styles.value}>{statusData.completedProjects}</span>
159+
160+
<p className={styles.label}>DELAYED PROJECTS</p>
161+
<span className={styles.value}>{statusData.delayedProjects}</span>
162+
</div>
163+
</div>
164+
</div>
165+
);
166+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
.container {
2+
width: 100%;
3+
margin-top: 20px;
4+
background: #ffffff;
5+
color: #111827;
6+
padding: 20px;
7+
border-radius: 12px;
8+
position: relative;
9+
border: 1px solid rgba(0, 0, 0, 0.12);
10+
}
11+
12+
.title {
13+
text-align: center;
14+
margin-bottom: 20px;
15+
font-size: 26px;
16+
font-weight: 600;
17+
}
18+
19+
.filterRow {
20+
display: flex;
21+
justify-content: center;
22+
gap: 10px;
23+
margin-bottom: 20px;
24+
}
25+
26+
.applyBtn {
27+
background: #4285f4;
28+
color: white;
29+
padding: 8px 16px;
30+
border-radius: 6px;
31+
border: none;
32+
cursor: pointer;
33+
}
34+
35+
.chartWrapper {
36+
display: flex;
37+
justify-content: center;
38+
align-items: center;
39+
gap: 80px;
40+
width: 100%;
41+
}
42+
43+
.summaryBox {
44+
flex: 0 0 280px;
45+
min-width: 260px;
46+
}
47+
48+
.label {
49+
font-size: 14px;
50+
font-weight: 700;
51+
color: #6b7280;
52+
}
53+
54+
.value {
55+
font-size: 22px;
56+
color: #d9534f;
57+
margin-bottom: 10px;
58+
}
59+
60+
@media (max-width: 768px) {
61+
.chartWrapper {
62+
flex-direction: column;
63+
gap: 28px;
64+
align-items: center;
65+
}
66+
67+
.chartContainer {
68+
flex: none;
69+
width: 92vw;
70+
max-width: 420px;
71+
height: 92vw;
72+
max-height: 420px;
73+
}
74+
75+
.summaryBox {
76+
width: 100%;
77+
align-items: center;
78+
text-align: center;
79+
}
80+
}
81+
.centerLabel {
82+
font-size: 16px;
83+
font-weight: 600;
84+
fill: currentColor;
85+
}
86+
87+
.centerValue {
88+
font-size: 18px;
89+
font-weight: 700;
90+
fill: currentColor;
91+
}
92+
93+
.noDataMessage {
94+
text-align: center;
95+
margin-top: 16px;
96+
font-weight: 500;
97+
}

0 commit comments

Comments
 (0)