diff --git a/src/features/areaAssignments/components/OrganizerMapRenderer.tsx b/src/features/areaAssignments/components/OrganizerMapRenderer.tsx index 659d8c05c..dc9d7d16b 100644 --- a/src/features/areaAssignments/components/OrganizerMapRenderer.tsx +++ b/src/features/areaAssignments/components/OrganizerMapRenderer.tsx @@ -22,6 +22,7 @@ import { ZetkinArea } from 'features/areas/types'; import { assigneesFilterContext } from './OrganizerMapFilters/AssigneeFilterContext'; import isPointInsidePolygon from '../../canvass/utils/isPointInsidePolygon'; import { getBoundSize } from '../../canvass/utils/getBoundSize'; +import { ZetkinPerson } from '../../../utils/types/zetkin'; const LocationMarker: FC<{ areaAssId: string; @@ -169,6 +170,158 @@ type OrganizerMapRendererProps = { sessions: ZetkinAreaAssignmentSession[]; }; +function HouseholdOverlayMarker(props: { + numberOfHouseholds: number; + numberOfLocations: number; +}) { + return ( + + {props.numberOfLocations} + + {props.numberOfHouseholds} + + ); +} + +function ProgressOverlayMarker(props: { + successfulVisitsColorPercent: number; + visitsColorPercent: number; +}) { + const theme = useTheme(); + + return ( + +
+ + ); +} + +function NumberOverlayMarker(props: { value: number }) { + const theme = useTheme(); + + return ( + + {props.value} + + ); +} + +function AssigneeOverlayMarker({ + organizationID, + people, + zoom, +}: { + organizationID: number; + people: ZetkinPerson[]; + zoom: number; +}) { + return ( + = 16 ? '95px' : '65px'} + > + {people.map((person, index) => { + if (index <= 4) { + return ( + + = 16 ? 'sm' : 'xs'} + url={`/api/orgs/${organizationID}/people/${person.id}/avatar`} + /> + + ); + } else if (index == 5) { + return ( + = 16 ? '30px' : '20px'} + justifyContent="center" + padding={1} + sx={{ boxShadow: '0 0 8px rgba(0,0,0,0.3)' }} + width={zoom >= 16 ? '30px' : '20px'} + > + = 16 ? 14 : 11} + >{`+${people.length - 5}`} + + ); + } else { + return null; + } + })} + + ); +} + const OrganizerMapRenderer: FC = ({ areas, areaStats, @@ -285,6 +438,28 @@ const OrganizerMapRenderer: FC = ({ } }); + const filteredAreas = areas + .map((area) => { + const people = sessions + .filter((session) => session.area.id == area.id) + .map((session) => session.assignee); + const hasPeople = !!people.length; + return { ...area, hasPeople }; + }) + .filter((area) => { + // Right now there is only one kind of filter + if (assigneesFilter === null) { + return true; + } + + if (area.hasPeople && assigneesFilter == 'unassigned') { + return false; + } else if (!area.hasPeople && assigneesFilter == 'assigned') { + return false; + } + return true; + }); + return ( <> @@ -297,7 +472,7 @@ const OrganizerMapRenderer: FC = ({ reactFGref.current = fgRef; }} > - {areas + {filteredAreas .sort((a0, a1) => { // Always render selected last, so that it gets // rendered on top of the unselected ones in case @@ -316,46 +491,13 @@ const OrganizerMapRenderer: FC = ({ .map((area) => { const selected = selectedId == area.id; - const mid: [number, number] = [0, 0]; - if (area.points.length) { - area.points - .map((input) => { - if ('lat' in input && 'lng' in input) { - return [input.lat as number, input.lng as number]; - } else { - return input; - } - }) - .forEach((point) => { - mid[0] += point[0]; - mid[1] += point[1]; - }); - - mid[0] /= area.points.length; - mid[1] /= area.points.length; - } - - const detailed = zoom >= 15; - - const people = sessions - .filter((session) => session.area.id == area.id) - .map((session) => session.assignee); - - const hasPeople = !!people.length; - - if (hasPeople && assigneesFilter == 'unassigned') { - return null; - } else if (!hasPeople && assigneesFilter == 'assigned') { - return null; - } - // The key changes when selected, to force redraw of polygon // to reflect new state through visual style const key = area.id + (selected ? '-selected' : '-default') + `-${areaStyle}` + - (hasPeople ? '-assigned' : ''); + (area.hasPeople ? '-assigned' : ''); const stats = areaStats.stats.find( (stat) => stat.areaId == area.id @@ -365,8 +507,6 @@ const OrganizerMapRenderer: FC = ({ locationsByAreaId[area.id].forEach( (location) => (numberOfHouseholds += location.households.length) ); - const numberOfLocations = locationsByAreaId[area.id].length; - const householdColorPercent = (numberOfHouseholds / highestHousholds) * 100; @@ -374,185 +514,32 @@ const OrganizerMapRenderer: FC = ({ ? (stats.num_visited_households / stats.num_households) * 100 : 0; - const successfulVisitsColorPercent = stats?.num_households - ? (stats.num_successful_visited_households / - stats.num_households) * - 100 - : 0; - return ( - <> - {overlayStyle == 'households' && ( - - - - {numberOfLocations} - - - - {numberOfHouseholds} - - - - )} - {overlayStyle == 'progress' && stats && ( - - -
- - + { + onSelectedIdChange(selected ? '' : area.id); + }, + }} + fillColor={getAreaColor( + area.hasPeople, + householdColorPercent, + visitsColorPercent )} - {overlayStyle == 'assignees' && hasPeople && ( - - {detailed && ( - = 16 ? '95px' : '65px'} - > - {people.map((person, index) => { - if (index <= 4) { - return ( - - = 16 ? 'sm' : 'xs'} - url={`/api/orgs/${assignment.organization.id}/people/${person.id}/avatar`} - /> - - ); - } else if (index == 5) { - return ( - = 16 ? '30px' : '20px'} - justifyContent="center" - padding={1} - sx={{ boxShadow: '0 0 8px rgba(0,0,0,0.3)' }} - width={zoom >= 16 ? '30px' : '20px'} - > - = 16 ? 14 : 11} - >{`+${people.length - 5}`} - - ); - } else { - return null; - } - })} - - )} - {!detailed && ( - - {people.length} - - )} - - )} - { - onSelectedIdChange(selected ? '' : area.id); - }, - }} - fillColor={getAreaColor( - hasPeople, - householdColorPercent, - visitsColorPercent - )} - fillOpacity={1} - interactive={areaStyle != 'hide'} - positions={area.points} - weight={selected ? 5 : 2} - /> - + fillOpacity={1} + interactive={areaStyle != 'hide'} + positions={area.points} + weight={selected ? 5 : 2} + /> ); })} - {locationStyle != 'hide' && - locations.map((location) => { + + {locationStyle != 'hide' && ( + + {locations.map((location) => { //Find ids of area/s that the location is in const areaIds: string[] = []; areas.forEach((area) => { @@ -616,6 +603,99 @@ const OrganizerMapRenderer: FC = ({ /> ); })} + + )} + + {filteredAreas.map((area) => { + const mid: [number, number] = [0, 0]; + if (area.points.length) { + area.points + .map((input) => { + if ('lat' in input && 'lng' in input) { + return [input.lat as number, input.lng as number]; + } else { + return input; + } + }) + .forEach((point) => { + mid[0] += point[0]; + mid[1] += point[1]; + }); + + mid[0] /= area.points.length; + mid[1] /= area.points.length; + } + + const detailed = zoom >= 15; + + const people = sessions + .filter((session) => session.area.id == area.id) + .map((session) => session.assignee); + + const stats = areaStats.stats.find((stat) => stat.areaId == area.id); + + let numberOfHouseholds = 0; + locationsByAreaId[area.id].forEach( + (location) => (numberOfHouseholds += location.households.length) + ); + const numberOfLocations = locationsByAreaId[area.id].length; + + const visitsColorPercent = stats?.num_households + ? (stats.num_visited_households / stats.num_households) * 100 + : 0; + + const successfulVisitsColorPercent = stats?.num_households + ? (stats.num_successful_visited_households / stats.num_households) * + 100 + : 0; + + const markerToRender = () => { + if (overlayStyle === 'households') { + return ( + + ); + } + if (overlayStyle == 'progress') { + return ( + + ); + } + if (overlayStyle === 'assignees' && area.hasPeople) { + if (detailed) { + return ( + + ); + } + return ; + } + return null; + }; + + const marker = markerToRender(); + if (marker === null) { + return null; + } + return ( + + {marker} + + ); + })} );