From 59e2f88ccee3c0878f73ed6b7c40b1b5f4524fa0 Mon Sep 17 00:00:00 2001 From: RoshiniSeelamsetty Date: Sat, 23 May 2026 21:43:02 -0400 Subject: [PATCH 1/2] (PRIORITY HIGH) Tanmay : Fix Mentor Label Text Visibility (In Dark Mode) in Role Distribution Chart --- src/components/TotalOrgSummary/PieChart.jsx | 88 +++--- .../TotalOrgSummary.module.css | 29 +- .../RoleDistributionPieChart.jsx | 260 +++++++++++------- 3 files changed, 232 insertions(+), 145 deletions(-) diff --git a/src/components/TotalOrgSummary/PieChart.jsx b/src/components/TotalOrgSummary/PieChart.jsx index a55b0f6e38..495033f954 100644 --- a/src/components/TotalOrgSummary/PieChart.jsx +++ b/src/components/TotalOrgSummary/PieChart.jsx @@ -1,44 +1,52 @@ -// // import * as React from 'react'; -// import { DefaultizedPieValueType } from '@mui/x-charts/models'; -// import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart'; +import * as React from 'react'; +import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart'; -// const data = [ -// { label: 'Group A', value: 400, color: '#0088FE' }, -// { label: 'Group B', value: 300, color: '#00C49F' }, -// { label: 'Group C', value: 300, color: '#FFBB28' }, -// { label: 'Group D', value: 200, color: '#FF8042' }, -// ]; +const data = [ + { id: 0, label: 'Administrator', value: 6, color: '#fb0505' }, + { id: 1, label: 'Volunteer', value: 4, color: '#8ebfff' }, + { id: 2, label: 'Owner', value: 3, color: '#f68d42' }, + { id: 3, label: 'Mentor', value: 1, color: '#f2ff00' }, +]; -// const sizing = { -// margin: { right: 5 }, -// width: 200, -// height: 200, -// legend: { hidden: true }, -// }; -// const TOTAL = data.map(item => item.value).reduce((a, b) => a + b, 0); +const TOTAL = data.reduce((sum, item) => sum + item.value, 0); -// const getArcLabel = (params: DefaultizedPieValueType) => { -// const percent = params.value / TOTAL; -// return `${(percent * 100).toFixed(0)}%`; -// }; +const getArcLabel = params => { + const percent = params.value / TOTAL; + return `${params.value}\n(${(percent * 100).toFixed(0)}%)`; +}; -// export default function PieChartWithCustomizedLabel() { -// return ( -// -// ); -// } +export default function RoleDistributionPieChart() { + return ( + + ); +} diff --git a/src/components/TotalOrgSummary/TotalOrgSummary.module.css b/src/components/TotalOrgSummary/TotalOrgSummary.module.css index 7e39bd65bd..f83cce2b7b 100644 --- a/src/components/TotalOrgSummary/TotalOrgSummary.module.css +++ b/src/components/TotalOrgSummary/TotalOrgSummary.module.css @@ -34,7 +34,7 @@ padding-top: 10px !important; padding-bottom: 10px !important; } - + .containerTotalOrgWrapper:global(.mb-5) { margin-bottom: 20px !important; } @@ -215,6 +215,29 @@ color: #fff !important; } +/* Role Distribution slice label overrides */ +.containerTotalOrgWrapper:global(.bg-oxford-blue) + .componentContainer + :global(.role-distribution-label-dark), +.containerTotalOrgWrapper:global(.bg-oxford-blue) + .componentContainer + :global(.role-distribution-label-dark) + tspan { + fill: #000 !important; + color: #000 !important; +} + +.containerTotalOrgWrapper:global(.bg-oxford-blue) + .componentContainer + :global(.role-distribution-label-light), +.containerTotalOrgWrapper:global(.bg-oxford-blue) + .componentContainer + :global(.role-distribution-label-light) + tspan { + fill: #fff !important; + color: #fff !important; +} + .containerTotalOrgWrapper.bg-oxford-blue h3 { color: #fff; } @@ -382,8 +405,6 @@ transform: translateX(4px) !important; } -/* Dark mode dropdown consistency */ - /* Component containers - Clean borderless design */ .componentContainer { margin: 0 0 15px; @@ -696,4 +717,4 @@ } } -/* stylelint-enable no-descending-specificity */ +/* stylelint-enable no-descending-specificity */ \ No newline at end of file diff --git a/src/components/TotalOrgSummary/VolunteerRolesTeamDynamics/RoleDistributionPieChart.jsx b/src/components/TotalOrgSummary/VolunteerRolesTeamDynamics/RoleDistributionPieChart.jsx index 54f126f1dd..fe95b7296b 100644 --- a/src/components/TotalOrgSummary/VolunteerRolesTeamDynamics/RoleDistributionPieChart.jsx +++ b/src/components/TotalOrgSummary/VolunteerRolesTeamDynamics/RoleDistributionPieChart.jsx @@ -1,21 +1,22 @@ +import React from 'react'; import { ResponsiveContainer, PieChart, Pie, Cell, Legend, Tooltip } from 'recharts'; import Loading from '~/components/common/Loading'; +import CustomTooltip from '../../CustomTooltip'; const COLORS = [ - '#2F80ED', // blue - '#56CCF2', // light blue - '#27AE60', // green - '#6FCF97', // light green - '#F2994A', // orange - '#F2C94C', // yellow - '#E14848', // red - '#9B51E0', // purple - '#F765A3', // pink - '#4F4F4F', // dark - '#828282', // grey + '#2F80ED', + '#56CCF2', + '#27AE60', + '#6FCF97', + '#F2994A', + '#F2C94C', + '#E14848', + '#9B51E0', + '#F765A3', + '#4F4F4F', + '#828282', ]; -// Explicit role-to-color mapping to keep key roles visually distinct and stable. const ROLE_COLOR_MAP = { Volunteer: '#8ebfff', Manager: '#27AE60', @@ -25,7 +26,19 @@ const ROLE_COLOR_MAP = { Mentor: '#f2ff00', }; -import CustomTooltip from '../../CustomTooltip'; +const RADIAN = Math.PI / 180; + +const getContrastTextColor = hexColor => { + const hex = hexColor.replace('#', ''); + + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + + return brightness > 160 ? '#000000' : '#FFFFFF'; +}; const RoleDistributionPieChart = ({ roleDistributionStats = [], isLoading, darkMode }) => { if (isLoading) { @@ -38,111 +51,153 @@ const RoleDistributionPieChart = ({ roleDistributionStats = [], isLoading, darkM ); } - roleDistributionStats.sort((a, b) => b.count - a.count); - const data = roleDistributionStats.map((item, index) => ({ + const sortedStats = [...roleDistributionStats].sort((a, b) => b.count - a.count); + + const data = sortedStats.map((item, index) => ({ name: item._id, value: item.count, - // Use a stable role mapping first, otherwise fallback by index. color: ROLE_COLOR_MAP[item._id] || COLORS[index % COLORS.length], })); + const totalValue = data.reduce((sum, entry) => sum + entry.value, 0); - const RADIAN = Math.PI / 180; - const renderCustomizedLabel = ({ - cx, - cy, - midAngle, - innerRadius, - outerRadius, - percent, - index, - }) => { - const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const renderLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index }) => { + if (percent <= 0.01 || !data[index]) return null; + + const slice = data[index]; + + const isSmallSlice = percent < 0.1; + + const labelColor = getContrastTextColor(slice.color); + + const labelClass = + labelColor === '#000000' ? 'role-distribution-label-dark' : 'role-distribution-label-light'; + + const radius = + innerRadius + (outerRadius - innerRadius) * (slice.name === 'Mentor' ? 0.34 : 0.52); + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); - return ( - - - {percent > 0.01 && ( - <> - - {`${data[index].value}`} - - - {`(${(percent * 100).toFixed(0)}%)`} - - - )} - - - - TOTAL ROLES - - - {data.length} - + if (isSmallSlice) { + return ( + + {`${slice.value} (${(percent * 100).toFixed(0)}%)`} - - ); - }; - - const renderCustomLegend = props => { - const { payload } = props; // payload is an array of legend items provided by Recharts + ); + } return ( -
    - {payload.map(entry => { - // 'entry.value' here corresponds to the 'nameKey' of the Pie (which we set to 'name') - const itemName = entry.value; - // Find the original data object to get the count and original color - const itemData = data.find(d => d.name === itemName); - - // If for some reason data is not found, skip rendering this legend item - if (!itemData) { - return null; - } - - const { value, color } = itemData; // 'value' is the count - const percentage = totalValue > 0 ? (value / totalValue) * 100 : 0; - - return ( -
  • -
    - - {`${itemName}: ${value} (${percentage.toFixed(1)}%)`} - -
  • - ); - })} -
+ + {slice.value} + + + + {`(${(percent * 100).toFixed(0)}%)`} + + ); }; + const renderCustomLegend = ({ payload }) => ( +
    + {payload.map(entry => { + const itemName = entry.value; + + const itemData = data.find(d => d.name === itemName); + + if (!itemData) return null; + + const { value, color } = itemData; + + const percentage = totalValue > 0 ? (value / totalValue) * 100 : 0; + + return ( +
  • +
    + + + {`${itemName}: ${value} (${percentage.toFixed(1)}%)`} + +
  • + ); + })} +
+ ); + return (
-
- +
+ {data.map(entry => ( ))} + + } /> From 8c5073b57da2aa8a39593646451076b7103f81b0 Mon Sep 17 00:00:00 2001 From: RoshiniSeelamsetty Date: Fri, 29 May 2026 17:14:17 -0400 Subject: [PATCH 2/2] fix(total-org-summary): resolve GlobalVolunteerMap rendering and location mapping issues --- .../GlobalVolunteerMap/GlobalVolunteerMap.jsx | 170 ++++++++++++------ 1 file changed, 112 insertions(+), 58 deletions(-) diff --git a/src/components/TotalOrgSummary/GlobalVolunteerMap/GlobalVolunteerMap.jsx b/src/components/TotalOrgSummary/GlobalVolunteerMap/GlobalVolunteerMap.jsx index 9b4ff9a0e8..2aad3a8f0f 100644 --- a/src/components/TotalOrgSummary/GlobalVolunteerMap/GlobalVolunteerMap.jsx +++ b/src/components/TotalOrgSummary/GlobalVolunteerMap/GlobalVolunteerMap.jsx @@ -1,8 +1,10 @@ -import { useEffect } from 'react'; +/* eslint-disable react/no-array-index-key */ +import { useEffect, useMemo } from 'react'; import L from 'leaflet'; import { MapContainer, TileLayer, useMap, CircleMarker } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import 'leaflet.heat'; + import Loading from '~/components/common/Loading'; const volunteerColors = { @@ -13,86 +15,138 @@ function HeatMap({ points }) { const map = useMap(); useEffect(() => { - if (points.length > 0) { - const heat = L.heatLayer(points, { - radius: 20, - blur: 20, - maxZoom: 2, - gradient: { - 0.4: '#00f', - 0.6: '#0f0', - 0.7: '#ff0', - 0.8: '#ffa500', - 1.0: '#f00', - }, - }).addTo(map); - - return () => { - map.removeLayer(heat); - }; - } - return undefined; - }, [points, map]); + if (!map || !points.length) return undefined; + + const heatLayer = L.heatLayer(points, { + radius: 20, + blur: 20, + maxZoom: 4, + gradient: { + 0.2: '#00f', + 0.4: '#0f0', + 0.6: '#ff0', + 0.8: '#ffa500', + 1.0: '#f00', + }, + }); + + heatLayer.addTo(map); + + return () => { + map.removeLayer(heatLayer); + }; + }, [map, points]); return null; } -function MapComponent({ locations = [], isLoading, error }) { - const heatMapPoints = (locations || []).map(location => [ - location._id.lat, - location._id.lng, - location.count, - ]); +function GlobalVolunteerMap({ locations = [], isLoading, error }) { + /** + * Normalize backend response safely + */ + const normalizedLocations = useMemo(() => { + if (!Array.isArray(locations)) return []; + + return locations + .map(location => { + const lat = location?._id?.lat ?? location?.lat ?? location?.latitude; + + const lng = location?._id?.lng ?? location?.lng ?? location?.longitude; - const activeVolunteers = (locations || []).filter(v => v.status === 'active'); + const count = Number(location?.count || 1); + + return { + lat: Number(lat), + lng: Number(lng), + count, + }; + }) + .filter( + item => + !Number.isNaN(item.lat) && + !Number.isNaN(item.lng) && + item.lat >= -90 && + item.lat <= 90 && + item.lng >= -180 && + item.lng <= 180, + ); + }, [locations]); + + /** + * Build heatmap points + */ + const heatMapPoints = useMemo( + () => normalizedLocations.map(location => [location.lat, location.lng, location.count]), + [normalizedLocations], + ); if (isLoading) { return (
-
+
); } + if (error) { + return ( +
+

Error loading map data

+
+ ); + } + + if (!normalizedLocations.length) { + return ( +
+

No volunteer location data available

+
+ ); + } + return (
- {error && ( -
-

Error: {error}

- -
- )} - + + - {activeVolunteers.length > 0 && - activeVolunteers.map(volunteer => ( - - ))} + + {normalizedLocations.map((location, index) => ( + + ))}
); } -export default MapComponent; +export default GlobalVolunteerMap;