turash/bugulma/frontend/components/map/OrganizationMarkers.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- Remove nested git repository from bugulma/frontend/.git
- Add all frontend files to main repository tracking
- Convert from separate frontend/backend repos to unified monorepo
- Preserve all frontend code and development history as tracked files
- Eliminate nested repository complexity for simpler development workflow

This creates a proper monorepo structure with frontend and backend
coexisting in the same repository for easier development and deployment.
2025-11-25 06:02:57 +01:00

191 lines
6.1 KiB
TypeScript

import L, { LatLngTuple } from 'leaflet';
import React, { useCallback, useMemo } from 'react';
import { Marker, Popup } from 'react-leaflet';
import { getSectorDisplay } from '@/constants.tsx';
import { useMapActions, useMapInteraction } from '@/contexts/MapContexts.tsx';
import { useOrganizationSites } from '@/hooks/map/useOrganizationSites.ts';
import { mapBackendSectorToTranslationKey } from '@/lib/sector-mapper.ts';
import { Organization } from '@/types.ts';
import { getCachedOrganizationIcon } from '@/utils/map/iconCache.ts';
interface OrganizationMarkersProps {
organizations: Organization[];
selectedOrg: Organization | null;
hoveredOrgId: string | null;
}
/**
* Individual marker component memoized to prevent unnecessary re-renders
*/
const OrganizationMarker = React.memo<{
org: Organization;
site: { Latitude: number; Longitude: number };
sector: { icon?: React.ReactElement; colorKey?: string } | null;
isSelected: boolean;
isHovered: boolean;
onSelect: (org: Organization) => void;
onHover: (orgId: string | null) => void;
}>(({ org, site, sector, isSelected, isHovered, onSelect, onHover }) => {
const position: LatLngTuple = useMemo(
() => [site.Latitude, site.Longitude],
[site.Latitude, site.Longitude]
);
const icon = useMemo(
() => getCachedOrganizationIcon(org.ID, org, sector, isSelected, isHovered),
[org.ID, org, sector, isSelected, isHovered]
);
const handleClick = useCallback(() => {
onSelect(org);
}, [org, onSelect]);
const handleMouseOver = useCallback(() => {
onHover(org.ID);
}, [org.ID, onHover]);
const handleMouseOut = useCallback(() => {
onHover(null);
}, [onHover]);
// Don't render if coordinates are invalid
if (!site.Latitude || !site.Longitude || site.Latitude === 0 || site.Longitude === 0) {
return null;
}
return (
<Marker
position={position}
icon={icon}
zIndexOffset={isSelected ? 1000 : isHovered ? 500 : 0}
keyboard={true}
interactive={true}
riseOnHover={true}
eventHandlers={{
click: handleClick,
mouseover: handleMouseOver,
mouseout: handleMouseOut,
}}
>
<Popup closeButton={true} autoPan={true} maxWidth={300}>
<div className="p-1">
<h3 className="text-base font-semibold mb-1">{org.Name}</h3>
{org.Sector && (
<p className="text-sm text-muted-foreground">{org.Sector}</p>
)}
{org.Description && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{org.Description}
</p>
)}
</div>
</Popup>
</Marker>
);
}, (prevProps, nextProps) => {
// Custom comparison function for React.memo
return (
prevProps.org.ID === nextProps.org.ID &&
prevProps.site.Latitude === nextProps.site.Latitude &&
prevProps.site.Longitude === nextProps.site.Longitude &&
prevProps.isSelected === nextProps.isSelected &&
prevProps.isHovered === nextProps.isHovered &&
prevProps.sector?.colorKey === nextProps.sector?.colorKey
);
});
OrganizationMarker.displayName = 'OrganizationMarker';
const OrganizationMarkers: React.FC<OrganizationMarkersProps> = ({
organizations,
selectedOrg,
hoveredOrgId,
}) => {
const { handleSelectOrg } = useMapActions();
const { setHoveredOrgId } = useMapInteraction();
const { orgSitesMap } = useOrganizationSites(organizations);
// No need for sector map - using getSectorDisplay directly
// Filter organizations that have valid coordinates (from site or organization)
const organizationsWithCoordinates = useMemo(() => {
const result = organizations
.filter((org) => org.ID && org.ID.trim() !== '')
.map((org) => {
const site = orgSitesMap.get(org.ID);
// Use site coordinates if available and valid
if (site && site.Latitude && site.Longitude && site.Latitude !== 0 && site.Longitude !== 0) {
return { org, site };
}
// Fallback to organization coordinates if available and valid
if (org.Latitude && org.Longitude && org.Latitude !== 0 && org.Longitude !== 0) {
return { org, site: { Latitude: org.Latitude, Longitude: org.Longitude } };
}
return null;
})
.filter(
(item): item is { org: Organization; site: { Latitude: number; Longitude: number } } =>
item !== null
);
return result;
}, [organizations, orgSitesMap]);
// Debug: Log marker count and details in development
if (process.env.NODE_ENV === 'development') {
console.log(`[OrganizationMarkers] Rendering ${organizationsWithCoordinates.length} markers`, {
totalOrganizations: organizations.length,
withCoordinates: organizationsWithCoordinates.length,
organizationsSample: organizations.slice(0, 3).map(org => ({
id: org.ID,
name: org.Name,
coords: [org.Latitude || 0, org.Longitude || 0],
})),
sample: organizationsWithCoordinates.slice(0, 3).map(({ org, site }) => ({
name: org.Name,
coords: site ? [site.Latitude, site.Longitude] : null,
hasLogo: !!org.LogoURL,
})),
});
// Additional debug info
if (organizations.length === 0) {
console.warn('[OrganizationMarkers] No organizations received! Check API connection and data flow.');
}
}
return (
<>
{organizationsWithCoordinates.map(({ org, site }) => {
const sectorDisplay = getSectorDisplay(org.Sector);
const isSelected = selectedOrg?.ID === org.ID;
const isHovered = hoveredOrgId === org.ID;
// Skip rendering if coordinates are invalid
if (!site.Latitude || !site.Longitude || site.Latitude === 0 || site.Longitude === 0) {
return null;
}
return (
<OrganizationMarker
key={`${org.ID}-${site.Latitude.toFixed(6)}-${site.Longitude.toFixed(6)}`}
org={org}
site={site}
sector={sectorDisplay}
isSelected={isSelected}
isHovered={isHovered}
onSelect={handleSelectOrg}
onHover={setHoveredOrgId}
/>
);
})}
</>
);
};
export default React.memo(OrganizationMarkers);