[31b88f42] Add staff locations and reach visualization to Heatmap Tool
This commit is contained in:
@@ -1,22 +1,44 @@
|
||||
// src/components/MapDisplay.tsx
|
||||
import React from 'react';
|
||||
import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, CircleMarker, Tooltip, Marker, Circle } from 'react-leaflet';
|
||||
import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.heat';
|
||||
import MarkerClusterGroup from 'react-leaflet-cluster';
|
||||
import type { HeatmapPoint, MapMode, TooltipColumn } from '../App';
|
||||
import L from 'leaflet';
|
||||
import type { HeatmapPoint, MapMode, TooltipColumn, StaffData, StaffLocation } from '../App';
|
||||
|
||||
// Fix for default Leaflet icon missing in some environments
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||
});
|
||||
|
||||
interface MapDisplayProps {
|
||||
heatmapData: HeatmapPoint[];
|
||||
radiusMultiplier: number;
|
||||
viewMode: MapMode;
|
||||
tooltipColumns: TooltipColumn[];
|
||||
staffData: StaffData;
|
||||
showSales: boolean;
|
||||
showTechnicians: boolean;
|
||||
travelRadius: number;
|
||||
}
|
||||
|
||||
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode, tooltipColumns }) => {
|
||||
const MapDisplay: React.FC<MapDisplayProps> = ({
|
||||
heatmapData,
|
||||
radiusMultiplier,
|
||||
viewMode,
|
||||
tooltipColumns,
|
||||
staffData,
|
||||
showSales,
|
||||
showTechnicians,
|
||||
travelRadius
|
||||
}) => {
|
||||
const germanyCenter: [number, number] = [51.1657, 10.4515];
|
||||
const maxCount = Math.max(...heatmapData.map(p => p.count), 1);
|
||||
const maxCount = heatmapData.length > 0 ? Math.max(...heatmapData.map(p => p.count), 1) : 1;
|
||||
|
||||
const calculateRadius = (count: number) => {
|
||||
return 3 + Math.log(count + 1) * 5 * radiusMultiplier;
|
||||
@@ -30,6 +52,24 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
|
||||
return '#66bd63'; // Green
|
||||
};
|
||||
|
||||
const salesIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
const technicianIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-orange.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
const renderPoints = () => (
|
||||
<MarkerClusterGroup>
|
||||
{heatmapData.map((point, idx) => (
|
||||
@@ -75,22 +115,68 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
|
||||
/>
|
||||
);
|
||||
|
||||
if (heatmapData.length === 0) {
|
||||
const renderStaff = () => {
|
||||
const elements: React.ReactNode[] = [];
|
||||
|
||||
if (showSales) {
|
||||
staffData.sales.forEach((person, idx) => {
|
||||
elements.push(
|
||||
<React.Fragment key={`sales-${idx}`}>
|
||||
<Marker position={[person.lat, person.lon]} icon={salesIcon}>
|
||||
<Tooltip permanent={false}>Sales: {person.name} ({person.plz})</Tooltip>
|
||||
</Marker>
|
||||
<Circle
|
||||
center={[person.lat, person.lon]}
|
||||
radius={travelRadius * 1000}
|
||||
pathOptions={{ color: 'blue', fillColor: 'blue', fillOpacity: 0.1 }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (showTechnicians) {
|
||||
staffData.technicians.forEach((person, idx) => {
|
||||
elements.push(
|
||||
<React.Fragment key={`tech-${idx}`}>
|
||||
<Marker position={[person.lat, person.lon]} icon={technicianIcon}>
|
||||
<Tooltip permanent={false}>Tech: {person.name} ({person.plz})</Tooltip>
|
||||
</Marker>
|
||||
<Circle
|
||||
center={[person.lat, person.lon]}
|
||||
radius={travelRadius * 1000}
|
||||
pathOptions={{ color: 'orange', fillColor: 'orange', fillOpacity: 0.1 }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const hasData = heatmapData.length > 0 || showSales || showTechnicians;
|
||||
|
||||
if (!hasData) {
|
||||
return (
|
||||
<div>
|
||||
<p>No data to display on the map.</p>
|
||||
<p>Upload a file and apply filters to see the heatmap.</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#888' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>No data to display on the map.</p>
|
||||
<p>Upload a file or enable staff layers to see something.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MapContainer key={viewMode} center={germanyCenter} zoom={6} style={{ height: '100%', width: '100%' }}>
|
||||
<MapContainer key={`${viewMode}-${showSales}-${showTechnicians}`} center={germanyCenter} zoom={6} style={{ height: '100%', width: '100%' }}>
|
||||
<TileLayer
|
||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
||||
/>
|
||||
{viewMode === 'points' ? renderPoints() : renderHeatmap()}
|
||||
{viewMode === 'points' && renderPoints()}
|
||||
{viewMode === 'heatmap' && renderHeatmap()}
|
||||
{renderStaff()}
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user