Iman_AMS/asm_app/src/pages/ActiveMap.tsx

863 lines
34 KiB
TypeScript

/**
* Active Map Page
*
* Displays hospitals and PHCC locations on an interactive map with markers showing:
* - Asset counts
* - Work Order counts (Normal/Urgent, by status)
* - Maintenance Log counts (Planned/Completed/Overdue)
*
* Supports both Hospital and PHCC location types with different field mappings:
* - Hospital: company field for assets/work orders, custom_hospital_name for maintenance
* - PHCC: custom_site for assets, site_name for work orders, asset-based for maintenance
*/
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { MapContainer, TileLayer, Marker, Popup, Tooltip, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import apiService from '../services/apiService';
import LinkField from '../components/LinkField';
import useUserDashboardFilters from '../hooks/useUserDashboardFilters';
import { isSiteEnabledHospital, buildMobileTeamSiteFilters } from '../utils/hospitalUtils';
// Fix for default marker icons in React-Leaflet
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
const DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
});
L.Marker.prototype.options.icon = DefaultIcon;
interface LocationData {
name: string;
latitude: number;
longitude: number;
location_type: 'hospital' | 'phcc';
assets: number;
normal_work_orders: number;
urgent_work_orders: number;
planned_maintenance: number;
completed_maintenance: number;
overdue_maintenance: number;
wo_open: number;
wo_progress: number;
wo_review: number;
wo_completed: number;
wo_closed: number;
phcc_asset_names?: string[];
}
// Component to handle map bounds fitting
const MapBounds: React.FC<{ locations: LocationData[] }> = ({ locations }) => {
const map = useMap();
useEffect(() => {
if (locations.length > 0 && locations.some(l => l.latitude && l.longitude)) {
const bounds = L.latLngBounds(
locations
.filter(l => l.latitude && l.longitude)
.map(l => [l.latitude, l.longitude] as [number, number])
);
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 8 });
} else {
// Fallback: show Saudi Arabia center
map.setView([24.8, 45.5], 6);
}
}, [locations, map]);
return null;
};
const ActiveMap: React.FC = () => {
const navigate = useNavigate();
const [selectedPHCC, setSelectedPHCC] = useState<string>('');
const [locations, setLocations] = useState<LocationData[]>([]);
const [loading, setLoading] = useState(true);
const markersRef = useRef<Record<string, L.Marker>>({});
const {
filterHospital,
filterSiteName,
hospitalLocked,
siteLocked,
allowedHospitals,
loading: filtersLoading,
handleHospitalChange,
handleSiteChange,
} = useUserDashboardFilters();
const showSiteFilter = isSiteEnabledHospital(filterHospital);
const hospitalLocationLinkFilters = useMemo(() => {
const filters: Record<string, unknown> = { custom_is_hospital: 1 };
if (allowedHospitals.length > 0) {
filters.name = ['in', allowedHospitals];
}
return filters;
}, [allowedHospitals]);
const mobileTeamSiteFilters = useMemo(
() => buildMobileTeamSiteFilters(filterHospital, siteLocked ? filterSiteName : undefined),
[filterHospital, filterSiteName, siteLocked]
);
const phccLocationLinkFilters = useMemo(() => {
const filters: Record<string, unknown> = { custom_is_phcc: 1 };
if (siteLocked && filterSiteName) {
filters.name = filterSiteName;
}
return filters;
}, [siteLocked, filterSiteName]);
const applyHospitalSiteFilters = useCallback(
(
filters: Record<string, unknown>,
locationType: 'hospital' | 'phcc',
siteField: 'site_name' | 'custom_site',
hospitalName?: string
) => {
const hospital = hospitalName || filterHospital;
if (
locationType === 'hospital' &&
filterSiteName &&
hospital &&
isSiteEnabledHospital(hospital)
) {
filters[siteField] = filterSiteName;
}
return filters;
},
[filterHospital, filterSiteName]
);
// Fetch location counts based on location type
const fetchLocationCounts = async (
location: { name: string; latitude: string | number; longitude: string | number },
locationType: 'hospital' | 'phcc'
): Promise<LocationData> => {
const isPhcc = locationType === 'phcc';
const assetFilterField = isPhcc ? 'custom_site' : 'company';
const woFilterField = isPhcc ? 'site_name' : 'company';
const counts: Partial<LocationData> = {
assets: 0,
normal_work_orders: 0,
urgent_work_orders: 0,
planned_maintenance: 0,
completed_maintenance: 0,
overdue_maintenance: 0,
wo_open: 0,
wo_progress: 0,
wo_review: 0,
wo_completed: 0,
wo_closed: 0,
phcc_asset_names: []
};
const hospitalName = locationType === 'hospital' ? location.name : undefined;
try {
const assetFilters = applyHospitalSiteFilters(
{ [assetFilterField]: location.name },
locationType,
'custom_site',
hospitalName
);
// Fetch Asset count
const assetsResponse = await apiService.apiCall<any>(
`/api/resource/Asset?filters=${encodeURIComponent(JSON.stringify(assetFilters))}&fields=["name"]&limit_page_length=0`
);
const assetList = assetsResponse?.data || [];
counts.assets = assetList.length;
// Store asset names for PHCC (needed for maintenance log queries)
if (isPhcc) {
counts.phcc_asset_names = assetList.map((a: any) => a.name);
}
const normalWOFilters = applyHospitalSiteFilters(
{
[woFilterField]: location.name,
custom_priority_: 'Normal',
repair_status: ['in', ['Open', 'Work In Progress']],
},
locationType,
'site_name',
hospitalName
);
// Fetch Normal Work Orders
const normalWOResponse = await apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(normalWOFilters))}&fields=["name"]`
);
counts.normal_work_orders = normalWOResponse?.data?.length || 0;
const urgentWOFilters = applyHospitalSiteFilters(
{
[woFilterField]: location.name,
custom_priority_: 'Urgent',
repair_status: ['in', ['Open', 'Work In Progress']],
},
locationType,
'site_name',
hospitalName
);
// Fetch Urgent Work Orders
const urgentWOResponse = await apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(urgentWOFilters))}&fields=["name"]`
);
counts.urgent_work_orders = urgentWOResponse?.data?.length || 0;
const buildWoStatusFilters = (repairStatus: string) =>
applyHospitalSiteFilters(
{
[woFilterField]: location.name,
repair_status: repairStatus,
},
locationType,
'site_name',
hospitalName
);
// Fetch WO Status counts
const [woOpen, woProgress, woReview, woCompleted, woClosed] = await Promise.all([
apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(buildWoStatusFilters('Open')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(buildWoStatusFilters('Work In Progress')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(buildWoStatusFilters('Pending Review')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(buildWoStatusFilters('Completed')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify(buildWoStatusFilters('Closed')))}&fields=["name"]`
),
]);
counts.wo_open = woOpen?.data?.length || 0;
counts.wo_progress = woProgress?.data?.length || 0;
counts.wo_review = woReview?.data?.length || 0;
counts.wo_completed = woCompleted?.data?.length || 0;
counts.wo_closed = woClosed?.data?.length || 0;
// Fetch Maintenance counts - different logic for PHCC vs Hospital
if (isPhcc && counts.phcc_asset_names && counts.phcc_asset_names.length > 0) {
// For PHCC, filter by asset_name
const [plannedPM, completedPM, overduePM] = await Promise.all([
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({
asset_name: ['in', counts.phcc_asset_names],
maintenance_status: 'Planned'
}))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({
asset_name: ['in', counts.phcc_asset_names],
maintenance_status: 'Completed'
}))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({
asset_name: ['in', counts.phcc_asset_names],
maintenance_status: 'Overdue'
}))}&fields=["name"]`
)
]);
counts.planned_maintenance = plannedPM?.data?.length || 0;
counts.completed_maintenance = completedPM?.data?.length || 0;
counts.overdue_maintenance = overduePM?.data?.length || 0;
} else if (!isPhcc) {
const buildMaintenanceFilters = (maintenanceStatus: string) =>
applyHospitalSiteFilters(
{
custom_hospital_name: location.name,
maintenance_status: maintenanceStatus,
},
locationType,
'site_name',
hospitalName
);
// For Hospital, filter by custom_hospital_name
const [plannedPM, completedPM, overduePM] = await Promise.all([
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify(buildMaintenanceFilters('Planned')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify(buildMaintenanceFilters('Completed')))}&fields=["name"]`
),
apiService.apiCall<any>(
`/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify(buildMaintenanceFilters('Overdue')))}&fields=["name"]`
),
]);
counts.planned_maintenance = plannedPM?.data?.length || 0;
counts.completed_maintenance = completedPM?.data?.length || 0;
counts.overdue_maintenance = overduePM?.data?.length || 0;
}
} catch (err) {
console.error(`Error fetching counts for ${location.name}:`, err);
}
return {
name: location.name,
latitude: parseFloat(location.latitude),
longitude: parseFloat(location.longitude),
location_type: locationType,
...counts
} as LocationData;
};
// Fetch locations and their counts
const fetchAndRenderData = async () => {
setLoading(true);
try {
let allLocations: LocationData[] = [];
const fetchPromises: Promise<LocationData[]>[] = [];
const effectivePhcc = siteLocked && filterSiteName ? filterSiteName : selectedPHCC;
// Fetch Hospital locations (if no PHCC is specifically selected, or if hospital is selected)
if (!effectivePhcc || filterHospital) {
const hospitalFilters: Record<string, unknown> = {
latitude: ['!=', ''],
longitude: ['!=', ''],
custom_is_hospital: 1,
};
if (filterHospital) {
hospitalFilters.name = filterHospital;
} else if (allowedHospitals.length > 0) {
hospitalFilters.name = ['in', allowedHospitals];
}
fetchPromises.push(
(async () => {
const locationsResponse = await apiService.apiCall<any>(
`/api/resource/Location?filters=${encodeURIComponent(JSON.stringify(hospitalFilters))}&fields=["name","latitude","longitude"]&limit_page_length=0`
);
const locationList = locationsResponse?.data || [];
const locationPromises = locationList.map((loc: { name: string; latitude: string; longitude: string }) =>
fetchLocationCounts(loc, 'hospital')
);
return Promise.all(locationPromises);
})()
);
}
// Fetch PHCC locations (if no hospital is specifically selected, or if PHCC is selected)
if (!filterHospital || effectivePhcc) {
const phccFilters: Record<string, unknown> = {
latitude: ['!=', ''],
longitude: ['!=', ''],
custom_is_phcc: 1,
};
if (effectivePhcc) {
phccFilters.name = effectivePhcc;
}
fetchPromises.push(
(async () => {
const locationsResponse = await apiService.apiCall<any>(
`/api/resource/Location?filters=${encodeURIComponent(JSON.stringify(phccFilters))}&fields=["name","latitude","longitude"]&limit_page_length=0`
);
const locationList = locationsResponse?.data || [];
const locationPromises = locationList.map((loc: { name: string; latitude: string; longitude: string }) =>
fetchLocationCounts(loc, 'phcc')
);
return Promise.all(locationPromises);
})()
);
}
const results = await Promise.all(fetchPromises);
allLocations = results.flat().filter((l) => !isNaN(l.latitude) && !isNaN(l.longitude));
setLocations(allLocations);
} catch (error) {
console.error('Error fetching map data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (filtersLoading) return;
if (siteLocked && filterSiteName) {
setSelectedPHCC(filterSiteName);
}
fetchAndRenderData();
}, [filterHospital, filterSiteName, selectedPHCC, filtersLoading, siteLocked, allowedHospitals]);
// Navigate to list view with filters
const navigateToWorkOrders = (location: LocationData, priority?: string, status?: string) => {
const params = new URLSearchParams();
if (location.location_type === 'phcc') {
params.set('site_name', location.name);
} else {
params.set('company', location.name);
if (filterSiteName && isSiteEnabledHospital(location.name)) {
params.set('site_name', filterSiteName);
}
}
if (priority) params.set('priority', priority);
if (status) params.set('status', status);
navigate(`/work-orders?${params.toString()}`);
};
const navigateToAssets = (location: LocationData) => {
const params = new URLSearchParams();
const filterField = location.location_type === 'phcc' ? 'custom_site' : 'company';
params.set(filterField, location.name);
navigate(`/assets?${params.toString()}`);
};
const navigateToMaintenanceCalendar = (location: LocationData, status?: string) => {
const params = new URLSearchParams();
if (location.location_type === 'phcc') {
// For PHCC, we need to pass asset names or use a different approach
params.set('phcc', location.name);
} else {
params.set('hospital', location.name);
}
if (status) params.set('status', status);
navigate(`/maintenance-calendar?${params.toString()}`);
};
// Create popup content with modern UI matching the application
const createPopupContent = (location: LocationData) => {
const isPhcc = location.location_type === 'phcc';
const typeBadge = isPhcc ? (
<span className="ml-2 px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 text-xs font-semibold rounded-full">
PHCC
</span>
) : (
<span className="ml-2 px-2 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs font-semibold rounded-full">
Hospital
</span>
);
return (
<div className="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg min-w-[280px] max-w-[320px]">
{/* Location Name Header */}
<div className="mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-bold text-gray-900 dark:text-white flex items-center flex-wrap">
{location.name}
{typeBadge}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Total Assets: <span className="font-semibold text-gray-900 dark:text-white">{location.assets}</span>
</p>
</div>
{/* Work Order Status Section */}
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">
Work Order Status
</h4>
<div className="flex gap-2 mb-3">
<button
onClick={() => navigateToWorkOrders(location, 'Normal')}
className="px-3 py-1.5 bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
Normal: {location.normal_work_orders}
</button>
<button
onClick={() => navigateToWorkOrders(location, 'Urgent')}
className="px-3 py-1.5 bg-red-100 dark:bg-red-900/30 hover:bg-red-200 dark:hover:bg-red-900/50 text-red-700 dark:text-red-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
Urgent: {location.urgent_work_orders}
</button>
</div>
{/* Status Table */}
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full text-xs">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-3 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Status</th>
<th className="px-3 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Count</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
<tr className="bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors">
<td className="px-3 py-2 text-red-800 dark:text-red-300 font-medium">Open</td>
<td className="px-3 py-2">
<button
onClick={() => navigateToWorkOrders(location, undefined, 'Open')}
className="text-red-700 dark:text-red-400 font-bold hover:underline cursor-pointer"
>
{location.wo_open}
</button>
</td>
</tr>
<tr className="bg-yellow-50 dark:bg-yellow-900/20 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors">
<td className="px-3 py-2 text-yellow-800 dark:text-yellow-300 font-medium">Work In Progress</td>
<td className="px-3 py-2">
<button
onClick={() => navigateToWorkOrders(location, undefined, 'Work In Progress')}
className="text-yellow-700 dark:text-yellow-400 font-bold hover:underline cursor-pointer"
>
{location.wo_progress}
</button>
</td>
</tr>
<tr className="bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors">
<td className="px-3 py-2 text-blue-800 dark:text-blue-300 font-medium">Pending Review</td>
<td className="px-3 py-2">
<button
onClick={() => navigateToWorkOrders(location, undefined, 'Pending Review')}
className="text-blue-700 dark:text-blue-400 font-bold hover:underline cursor-pointer"
>
{location.wo_review}
</button>
</td>
</tr>
<tr className="bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors">
<td className="px-3 py-2 text-green-800 dark:text-green-300 font-medium">Completed</td>
<td className="px-3 py-2">
<button
onClick={() => navigateToWorkOrders(location, undefined, 'Completed')}
className="text-green-700 dark:text-green-400 font-bold hover:underline cursor-pointer"
>
{location.wo_completed}
</button>
</td>
</tr>
<tr className="bg-gray-50 dark:bg-gray-700/40 hover:bg-gray-100 dark:hover:bg-gray-700/60 transition-colors">
<td className="px-3 py-2 text-gray-800 dark:text-gray-300 font-medium">Closed</td>
<td className="px-3 py-2">
<button
onClick={() => navigateToWorkOrders(location, undefined, 'Closed')}
className="text-gray-700 dark:text-gray-300 font-bold hover:underline cursor-pointer"
>
{location.wo_closed}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Preventive Maintenance Section */}
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">
Preventive Maintenance
</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => navigateToMaintenanceCalendar(location, 'Planned')}
className="px-3 py-1.5 bg-orange-100 dark:bg-orange-900/30 hover:bg-orange-200 dark:hover:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
Planned: {location.planned_maintenance}
</button>
<button
onClick={() => navigateToMaintenanceCalendar(location, 'Completed')}
className="px-3 py-1.5 bg-green-100 dark:bg-green-900/30 hover:bg-green-200 dark:hover:bg-green-900/50 text-green-700 dark:text-green-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
Completed: {location.completed_maintenance}
</button>
<button
onClick={() => navigateToMaintenanceCalendar(location, 'Overdue')}
className="px-3 py-1.5 bg-red-100 dark:bg-red-900/30 hover:bg-red-200 dark:hover:bg-red-900/50 text-red-700 dark:text-red-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
Overdue: {location.overdue_maintenance}
</button>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2 pt-3 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => navigateToAssets(location)}
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors cursor-pointer"
>
View Assets
</button>
<button
onClick={() => navigateToWorkOrders(location)}
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600 text-white rounded-lg text-sm font-medium transition-colors cursor-pointer"
>
View Work Orders
</button>
</div>
</div>
);
};
return (
<div className="h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
<div className="flex-shrink-0 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 px-4 py-3">
<h1 className="text-xl font-semibold text-gray-800 dark:text-white">Active Map</h1>
</div>
{/* Filter Container */}
<div className="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 relative z-[1000]">
<div className="flex flex-wrap gap-4 relative z-[1000]">
{/* Hospital Filter — Location name matches Company/Hospital name */}
<div className="w-64 relative z-[1000]">
<LinkField
label="Hospital"
doctype="Location"
value={filterHospital}
onChange={handleHospitalChange}
filters={hospitalLocationLinkFilters}
placeholder="All Hospitals"
disabled={hospitalLocked}
/>
</div>
{showSiteFilter && (
<div className="w-64 relative z-[1000]">
<LinkField
label="Site Name"
doctype="Mobile Team Site"
value={filterSiteName}
onChange={handleSiteChange}
filters={mobileTeamSiteFilters}
placeholder="All Sites"
disabled={siteLocked}
/>
</div>
)}
{/* PHCC Filter */}
<div className="w-64 relative z-[1000]">
<LinkField
label="PHCC"
doctype="Location"
value={siteLocked && filterSiteName ? filterSiteName : selectedPHCC}
onChange={setSelectedPHCC}
filters={phccLocationLinkFilters}
placeholder="Select PHCC"
disabled={siteLocked}
/>
</div>
</div>
</div>
{/* Map Container */}
<div className="flex-1 relative" style={{ zIndex: 1 }}>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75 z-[1000]">
<div className="text-gray-600 dark:text-gray-300">
{filtersLoading ? 'Loading filters...' : 'Loading map data...'}
</div>
</div>
)}
<MapContainer
center={[24.8, 45.5]}
zoom={6}
style={{ height: '100%', width: '100%' }}
zoomControl={true}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapBounds locations={locations} />
{locations.map((location) => {
const urgentIndicator = location.urgent_work_orders > 0 ? '🚨 URGENT! ' : '';
const isPhcc = location.location_type === 'phcc';
const typeIndicator = isPhcc ? '🏥 PHCC' : '🏨 Hospital';
const markerKey = `${location.name}-${location.latitude}-${location.longitude}`;
return (
<Marker
key={markerKey}
position={[location.latitude, location.longitude]}
ref={(ref) => {
if (ref) {
markersRef.current[markerKey] = ref;
// Apply marker styling based on location type and urgency
setTimeout(() => {
const markerElement = ref.getElement();
if (markerElement) {
// Remove all custom classes first
markerElement.classList.remove('urgent-marker', 'red-marker', 'phcc-marker');
if (location.urgent_work_orders > 0) {
// Same red flashing for both Hospital and PHCC urgent markers
markerElement.classList.add('urgent-marker', 'red-marker');
} else if (isPhcc) {
// Green marker for non-urgent PHCC
markerElement.classList.add('phcc-marker');
}
// Non-urgent hospitals use default blue marker
}
}, 100);
}
}}
>
<Tooltip
permanent={false}
direction="right"
className="hospital-tooltip-modern"
>
<div className="p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg min-w-[200px]">
<div className="mb-2 pb-2 border-b border-gray-200 dark:border-gray-700">
<h4 className="text-sm font-bold text-gray-900 dark:text-white">
{urgentIndicator}{location.name}
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{typeIndicator}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
Assets: <span className="font-semibold text-gray-900 dark:text-white">{location.assets}</span>
</p>
</div>
<div className="space-y-1 text-xs">
<div className="flex items-center justify-between">
<span className="text-gray-600 dark:text-gray-400">Normal WOs:</span>
<span className="font-semibold text-blue-700 dark:text-blue-300">{location.normal_work_orders}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600 dark:text-gray-400">Urgent WOs:</span>
<span className="font-semibold text-red-700 dark:text-red-300">{location.urgent_work_orders}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600 dark:text-gray-400">Planned PMs:</span>
<span className="font-semibold text-orange-700 dark:text-orange-300">{location.planned_maintenance}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600 dark:text-gray-400">Completed PMs:</span>
<span className="font-semibold text-green-700 dark:text-green-300">{location.completed_maintenance}</span>
</div>
</div>
</div>
</Tooltip>
<Popup
className={isPhcc ? "phcc-popup-container" : "hospital-popup-container"}
maxWidth={320}
maxHeight={450}
autoPan={true}
keepInView={true}
closeButton={true}
autoClose={false}
>
{createPopupContent(location)}
</Popup>
</Marker>
);
})}
</MapContainer>
</div>
{/* Custom Styles */}
<style>{`
/* Ensure filter container and dropdowns stay above map */
.leaflet-container {
z-index: 1 !important;
}
/* LinkField dropdown z-index - ensure it's above everything */
[data-linkfield-dropdown],
.linkfield-dropdown,
.react-select__menu,
.react-select__menu-portal,
.select2-container,
.select2-dropdown {
z-index: 1050 !important;
}
/* Any dropdown menu from LinkField */
div[role="listbox"],
ul[role="listbox"],
.dropdown-menu,
.autocomplete-dropdown {
z-index: 1050 !important;
}
.hospital-tooltip-modern {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.hospital-tooltip-modern .leaflet-tooltip-content-wrapper {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
.hospital-tooltip-modern .leaflet-tooltip-content {
margin: 0 !important;
}
.hospital-popup-container .leaflet-popup-content-wrapper {
padding: 0;
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.hospital-popup-container .leaflet-popup-content {
margin: 0;
width: auto !important;
}
/* PHCC Popup Container - with green left border */
.phcc-popup-container .leaflet-popup-content-wrapper {
padding: 0;
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-left: 4px solid #28a745;
}
.phcc-popup-container .leaflet-popup-content {
margin: 0;
width: auto !important;
}
/* Red Flashing Animation for Urgent Work Orders (Both Hospital & PHCC) */
/* Fixed: Stays red throughout, just pulses brighter */
.urgent-marker {
animation: urgent-flash 2s infinite;
}
@keyframes urgent-flash {
0%, 50% {
filter: hue-rotate(120deg) saturate(2) brightness(0.8);
}
25%, 75% {
filter: hue-rotate(120deg) saturate(2.5) brightness(1.5) drop-shadow(0 0 10px red);
}
}
/* Red marker style (base state for urgent) */
.red-marker {
filter: hue-rotate(120deg) saturate(2) brightness(0.8);
}
/* Green marker style for PHCC */
.phcc-marker {
filter: hue-rotate(-120deg) saturate(1.3) brightness(1.1);
}
.leaflet-popup {
z-index: 2000 !important;
}
.leaflet-tooltip {
z-index: 2000 !important;
}
`}</style>
</div>
);
};
export default ActiveMap;