Skip to content

Commit bab7d81

Browse files
Merge pull request #5305 from OneCommunityGlobal/Roshini-Seelamsetty-Fix-Global-Volunteer-Network-map-Not-Populating-(Total-Org-Summary-Dashboard)
Roshini Seelamsetty : Fix Global Volunteer Network map Not Populating (Total Org Summary Dashboard)
2 parents beefbf0 + 13811fe commit bab7d81

4 files changed

Lines changed: 347 additions & 209 deletions

File tree

Lines changed: 112 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { useEffect } from 'react';
1+
/* eslint-disable react/no-array-index-key */
2+
import { useEffect, useMemo } from 'react';
23
import L from 'leaflet';
34
import { MapContainer, TileLayer, useMap, CircleMarker } from 'react-leaflet';
45
import 'leaflet/dist/leaflet.css';
56
import 'leaflet.heat';
7+
68
import Loading from '~/components/common/Loading';
79

810
const volunteerColors = {
@@ -13,86 +15,138 @@ function HeatMap({ points }) {
1315
const map = useMap();
1416

1517
useEffect(() => {
16-
if (points.length > 0) {
17-
const heat = L.heatLayer(points, {
18-
radius: 20,
19-
blur: 20,
20-
maxZoom: 2,
21-
gradient: {
22-
0.4: '#00f',
23-
0.6: '#0f0',
24-
0.7: '#ff0',
25-
0.8: '#ffa500',
26-
1.0: '#f00',
27-
},
28-
}).addTo(map);
29-
30-
return () => {
31-
map.removeLayer(heat);
32-
};
33-
}
34-
return undefined;
35-
}, [points, map]);
18+
if (!map || !points.length) return undefined;
19+
20+
const heatLayer = L.heatLayer(points, {
21+
radius: 20,
22+
blur: 20,
23+
maxZoom: 4,
24+
gradient: {
25+
0.2: '#00f',
26+
0.4: '#0f0',
27+
0.6: '#ff0',
28+
0.8: '#ffa500',
29+
1.0: '#f00',
30+
},
31+
});
32+
33+
heatLayer.addTo(map);
34+
35+
return () => {
36+
map.removeLayer(heatLayer);
37+
};
38+
}, [map, points]);
3639

3740
return null;
3841
}
3942

40-
function MapComponent({ locations = [], isLoading, error }) {
41-
const heatMapPoints = (locations || []).map(location => [
42-
location._id.lat,
43-
location._id.lng,
44-
location.count,
45-
]);
43+
function GlobalVolunteerMap({ locations = [], isLoading, error }) {
44+
/**
45+
* Normalize backend response safely
46+
*/
47+
const normalizedLocations = useMemo(() => {
48+
if (!Array.isArray(locations)) return [];
49+
50+
return locations
51+
.map(location => {
52+
const lat = location?._id?.lat ?? location?.lat ?? location?.latitude;
53+
54+
const lng = location?._id?.lng ?? location?.lng ?? location?.longitude;
4655

47-
const activeVolunteers = (locations || []).filter(v => v.status === 'active');
56+
const count = Number(location?.count || 1);
57+
58+
return {
59+
lat: Number(lat),
60+
lng: Number(lng),
61+
count,
62+
};
63+
})
64+
.filter(
65+
item =>
66+
!Number.isNaN(item.lat) &&
67+
!Number.isNaN(item.lng) &&
68+
item.lat >= -90 &&
69+
item.lat <= 90 &&
70+
item.lng >= -180 &&
71+
item.lng <= 180,
72+
);
73+
}, [locations]);
74+
75+
/**
76+
* Build heatmap points
77+
*/
78+
const heatMapPoints = useMemo(
79+
() => normalizedLocations.map(location => [location.lat, location.lng, location.count]),
80+
[normalizedLocations],
81+
);
4882

4983
if (isLoading) {
5084
return (
5185
<div className="d-flex justify-content-center align-items-center">
52-
<div className="w-100vh">
86+
<div className="w-100">
5387
<Loading />
5488
</div>
5589
</div>
5690
);
5791
}
5892

93+
if (error) {
94+
return (
95+
<div className="error-container text-center p-4">
96+
<p>Error loading map data</p>
97+
</div>
98+
);
99+
}
100+
101+
if (!normalizedLocations.length) {
102+
return (
103+
<div
104+
className="d-flex justify-content-center align-items-center"
105+
style={{
106+
height: '500px',
107+
border: '1px solid #ddd',
108+
borderRadius: '8px',
109+
}}
110+
>
111+
<p>No volunteer location data available</p>
112+
</div>
113+
);
114+
}
115+
59116
return (
60117
<div className="map-container" style={{ marginTop: '20px' }}>
61-
{error && (
62-
<div className="error-container">
63-
<p>Error: {error}</p>
64-
<button
65-
type="button"
66-
onClick={() => {
67-
/* Add retry logic here */
68-
}}
69-
>
70-
Retry
71-
</button>
72-
</div>
73-
)}
74-
<MapContainer center={[20, 0]} zoom={2} style={{ height: '500px', width: '100%' }}>
118+
<MapContainer
119+
center={[20, 0]}
120+
zoom={2}
121+
scrollWheelZoom
122+
style={{
123+
height: '500px',
124+
width: '100%',
125+
borderRadius: '8px',
126+
}}
127+
>
75128
<TileLayer
76129
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
77-
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
130+
attribution="&copy; OpenStreetMap contributors"
78131
/>
132+
79133
<HeatMap points={heatMapPoints} />
80-
{activeVolunteers.length > 0 &&
81-
activeVolunteers.map(volunteer => (
82-
<CircleMarker
83-
key={`active-${volunteer._id.lat}-${volunteer._id.lng}`}
84-
center={[volunteer._id.lat, volunteer._id.lng]}
85-
radius={8}
86-
pathOptions={{
87-
color: volunteerColors.active,
88-
fillColor: volunteerColors.active,
89-
fillOpacity: 0.8,
90-
}}
91-
/>
92-
))}
134+
135+
{normalizedLocations.map((location, index) => (
136+
<CircleMarker
137+
key={`${location.lat}-${location.lng}-${index}`}
138+
center={[location.lat, location.lng]}
139+
radius={Math.min(10, Math.max(4, location.count))}
140+
pathOptions={{
141+
color: volunteerColors.active,
142+
fillColor: volunteerColors.active,
143+
fillOpacity: 0.7,
144+
}}
145+
/>
146+
))}
93147
</MapContainer>
94148
</div>
95149
);
96150
}
97151

98-
export default MapComponent;
152+
export default GlobalVolunteerMap;
Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,52 @@
1-
// // import * as React from 'react';
2-
// import { DefaultizedPieValueType } from '@mui/x-charts/models';
3-
// import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart';
1+
import * as React from 'react';
2+
import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart';
43

5-
// const data = [
6-
// { label: 'Group A', value: 400, color: '#0088FE' },
7-
// { label: 'Group B', value: 300, color: '#00C49F' },
8-
// { label: 'Group C', value: 300, color: '#FFBB28' },
9-
// { label: 'Group D', value: 200, color: '#FF8042' },
10-
// ];
4+
const data = [
5+
{ id: 0, label: 'Administrator', value: 6, color: '#fb0505' },
6+
{ id: 1, label: 'Volunteer', value: 4, color: '#8ebfff' },
7+
{ id: 2, label: 'Owner', value: 3, color: '#f68d42' },
8+
{ id: 3, label: 'Mentor', value: 1, color: '#f2ff00' },
9+
];
1110

12-
// const sizing = {
13-
// margin: { right: 5 },
14-
// width: 200,
15-
// height: 200,
16-
// legend: { hidden: true },
17-
// };
18-
// const TOTAL = data.map(item => item.value).reduce((a, b) => a + b, 0);
11+
const TOTAL = data.reduce((sum, item) => sum + item.value, 0);
1912

20-
// const getArcLabel = (params: DefaultizedPieValueType) => {
21-
// const percent = params.value / TOTAL;
22-
// return `${(percent * 100).toFixed(0)}%`;
23-
// };
13+
const getArcLabel = params => {
14+
const percent = params.value / TOTAL;
15+
return `${params.value}\n(${(percent * 100).toFixed(0)}%)`;
16+
};
2417

25-
// export default function PieChartWithCustomizedLabel() {
26-
// return (
27-
// <PieChart
28-
// series={[
29-
// {
30-
// outerRadius: 80,
31-
// data,
32-
// arcLabel: getArcLabel,
33-
// },
34-
// ]}
35-
// sx={{
36-
// [`& .${pieArcLabelClasses.root}`]: {
37-
// fill: 'white',
38-
// fontSize: 14,
39-
// },
40-
// }}
41-
// {...sizing}
42-
// />
43-
// );
44-
// }
18+
export default function RoleDistributionPieChart() {
19+
return (
20+
<PieChart
21+
series={[
22+
{
23+
data,
24+
innerRadius: 70,
25+
outerRadius: 145,
26+
arcLabel: getArcLabel,
27+
arcLabelMinAngle: 10,
28+
arcLabelRadius: 105,
29+
cornerRadius: 0,
30+
paddingAngle: 0,
31+
},
32+
]}
33+
width={520}
34+
height={430}
35+
slotProps={{
36+
legend: {
37+
hidden: false,
38+
},
39+
}}
40+
sx={{
41+
[`& .${pieArcLabelClasses.root}`]: {
42+
fill: '#ffffff',
43+
fontSize: 14,
44+
fontWeight: 700,
45+
},
46+
[`& .${pieArcLabelClasses.root}`]: {
47+
whiteSpace: 'pre',
48+
},
49+
}}
50+
/>
51+
);
52+
}

src/components/TotalOrgSummary/TotalOrgSummary.module.css

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
padding-top: 10px !important;
3535
padding-bottom: 10px !important;
3636
}
37-
37+
3838
.containerTotalOrgWrapper:global(.mb-5) {
3939
margin-bottom: 20px !important;
4040
}
@@ -215,6 +215,29 @@
215215
color: #fff !important;
216216
}
217217

218+
/* Role Distribution slice label overrides */
219+
.containerTotalOrgWrapper:global(.bg-oxford-blue)
220+
.componentContainer
221+
:global(.role-distribution-label-dark),
222+
.containerTotalOrgWrapper:global(.bg-oxford-blue)
223+
.componentContainer
224+
:global(.role-distribution-label-dark)
225+
tspan {
226+
fill: #000 !important;
227+
color: #000 !important;
228+
}
229+
230+
.containerTotalOrgWrapper:global(.bg-oxford-blue)
231+
.componentContainer
232+
:global(.role-distribution-label-light),
233+
.containerTotalOrgWrapper:global(.bg-oxford-blue)
234+
.componentContainer
235+
:global(.role-distribution-label-light)
236+
tspan {
237+
fill: #fff !important;
238+
color: #fff !important;
239+
}
240+
218241
.containerTotalOrgWrapper.bg-oxford-blue h3 {
219242
color: #fff;
220243
}
@@ -382,8 +405,6 @@
382405
transform: translateX(4px) !important;
383406
}
384407

385-
/* Dark mode dropdown consistency */
386-
387408
/* Component containers - Clean borderless design */
388409
.componentContainer {
389410
margin: 0 0 15px;
@@ -696,4 +717,4 @@
696717
}
697718
}
698719

699-
/* stylelint-enable no-descending-specificity */
720+
/* stylelint-enable no-descending-specificity */

0 commit comments

Comments
 (0)