diff --git a/src/components/BMDashboard/BMDashboard.jsx b/src/components/BMDashboard/BMDashboard.jsx index 56087ba420..919bc8008f 100644 --- a/src/components/BMDashboard/BMDashboard.jsx +++ b/src/components/BMDashboard/BMDashboard.jsx @@ -4,6 +4,7 @@ import { Container } from 'reactstrap'; import { fetchBMProjects } from '../../actions/bmdashboard/projectActions'; import ProjectsList from './Projects/ProjectsList'; import ProjectSelectForm from './Projects/ProjectSelectForm'; +import ProjectStatusDonutChart from './ProjectStatus/ProjectStatusDonutChart'; import BMError from './shared/BMError'; import './BMDashboard.css'; @@ -36,6 +37,7 @@ export function BMDashboard() { ) : ( <> + )} diff --git a/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.jsx b/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.jsx new file mode 100644 index 0000000000..661255dab9 --- /dev/null +++ b/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.jsx @@ -0,0 +1,166 @@ +import React, { useEffect, useState } from 'react'; +import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, Label } from 'recharts'; +import axios from 'axios'; +import styles from './ProjectStatusDonutChart.module.css'; + +const COLORS = ['#B39DDB', '#80DEEA', '#FFABAB']; // Active, Completed, Delayed + +export default function ProjectStatusDonutChart() { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [statusData, setStatusData] = useState(null); + + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + const fetchStatus = async () => { + try { + setLoading(true); + setError(null); + + // Build query string + const query = []; + if (startDate) query.push(`startDate=${startDate}`); + if (endDate) query.push(`endDate=${endDate}`); + const queryString = query.length ? `?${query.join('&')}` : ''; + + // Get token from localStorage (Dev Admin session) + const token = localStorage.getItem('token'); + + const res = await axios.get(`http://localhost:4500/api/projects/status${queryString}`, { + headers: { Authorization: token }, + }); + + // TEMPORARY MOCK DATA - for testing purposes + /*setStatusData({ + totalProjects: 50, + activeProjects: 20, + completedProjects: 20, + delayedProjects: 10, + }); + return;*/ + + setStatusData(res.data); + } catch (err) { + // console.error(err); + setError('Unable to load project status.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStatus(); + }, []); + + if (loading) return

Loading project status...

; + if (error) return

{error}

; + if (!statusData) return

No data available.

; + + const pieData = [ + { name: 'Active Projects', value: statusData.activeProjects }, + { name: 'Completed Projects', value: statusData.completedProjects }, + { name: 'Delayed Projects', value: statusData.delayedProjects }, + ]; + + // SHOW MESSAGE WHEN THERE IS NO DATA + if (pieData.every(item => item.value === 0)) { + return ( +
+

PROJECT STATUS

+

No project status data available.

+
+ ); + } + + const today = new Date().toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + const allZero = + !statusData.activeProjects && !statusData.completedProjects && !statusData.delayedProjects; + + return ( +
+

PROJECT STATUS

+ +
+ setStartDate(e.target.value)} + /> + + setEndDate(e.target.value)} + /> + + +
+ +
+ {/* Only draw the ring if at least one status has data */} + {!allZero && ( + + + + {pieData.map((entry, index) => ( + + ))} + + + + + + + )} + +
+

{today}

+ +

ACTIVE PROJECTS

+ {statusData.activeProjects} + +

COMPLETED PROJECTS

+ {statusData.completedProjects} + +

DELAYED PROJECTS

+ {statusData.delayedProjects} +
+
+
+ ); +} diff --git a/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.module.css b/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.module.css new file mode 100644 index 0000000000..0d9e347cf3 --- /dev/null +++ b/src/components/BMDashboard/ProjectStatus/ProjectStatusDonutChart.module.css @@ -0,0 +1,97 @@ +.container { + width: 100%; + margin-top: 20px; + background: #ffffff; + color: #111827; + padding: 20px; + border-radius: 12px; + position: relative; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +.title { + text-align: center; + margin-bottom: 20px; + font-size: 26px; + font-weight: 600; +} + +.filterRow { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 20px; +} + +.applyBtn { + background: #4285f4; + color: white; + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; +} + +.chartWrapper { + display: flex; + justify-content: center; + align-items: center; + gap: 80px; + width: 100%; +} + +.summaryBox { + flex: 0 0 280px; + min-width: 260px; +} + +.label { + font-size: 14px; + font-weight: 700; + color: #6b7280; +} + +.value { + font-size: 22px; + color: #d9534f; + margin-bottom: 10px; +} + +@media (max-width: 768px) { + .chartWrapper { + flex-direction: column; + gap: 28px; + align-items: center; + } + + .chartContainer { + flex: none; + width: 92vw; + max-width: 420px; + height: 92vw; + max-height: 420px; + } + + .summaryBox { + width: 100%; + align-items: center; + text-align: center; + } +} +.centerLabel { + font-size: 16px; + font-weight: 600; + fill: currentColor; +} + +.centerValue { + font-size: 18px; + font-weight: 700; + fill: currentColor; +} + +.noDataMessage { + text-align: center; + margin-top: 16px; + font-weight: 500; +} \ No newline at end of file