From f9e16bc8adc6d58b0e69ad4cfbbbd818136638f5 Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 9 Mar 2026 13:44:15 +0000 Subject: [PATCH] [31b88f42] Add staff locations and reach visualization to Heatmap Tool --- heatmap-tool/backend/main.py | 40 +++++++ heatmap-tool/frontend/src/App.tsx | 60 ++++++++++ .../frontend/src/components/MapDisplay.tsx | 106 ++++++++++++++++-- 3 files changed, 196 insertions(+), 10 deletions(-) diff --git a/heatmap-tool/backend/main.py b/heatmap-tool/backend/main.py index 080d872d..5c887e33 100644 --- a/heatmap-tool/backend/main.py +++ b/heatmap-tool/backend/main.py @@ -21,6 +21,22 @@ df_storage = None plz_column_name = None plz_geocoord_df = None +STAFF_DATA = { + "sales": [ + {"name": "Alexander Kiss", "plz": "86911"}, + {"name": "Sebastian Hosbach", "plz": "44135"}, + {"name": "Pierre Holein", "plz": "65428"}, + {"name": "Daniel Sengstock", "plz": "24939"}, + ], + "technicians": [ + {"name": "Merlin Kraus", "plz": "84489"}, + {"name": "Bledar Haxijaj", "plz": "85570"}, + {"name": "Aaron Schmelting", "plz": "44649"}, + {"name": "Sergej", "plz": "97070"}, + {"name": "Christoph Kadlubek", "plz": "30159"}, + ] +} + @app.on_event("startup") def load_plz_data(): global plz_geocoord_df @@ -59,6 +75,30 @@ class PlzColumnRequest(BaseModel): def read_root(): return {"message": "Heatmap Tool Backend"} +@app.get("/api/staff-locations") +async def get_staff_locations(): + global plz_geocoord_df + if plz_geocoord_df.empty: + raise HTTPException(status_code=500, detail="Geocoding data is not available on the server.") + + result = {"sales": [], "technicians": []} + + for category in ["sales", "technicians"]: + for person in STAFF_DATA[category]: + plz = person["plz"] + if plz in plz_geocoord_df.index: + coords = plz_geocoord_df.loc[plz] + result[category].append({ + "name": person["name"], + "plz": plz, + "lat": float(coords["y"]), + "lon": float(coords["x"]) + }) + else: + print(f"WARNING: PLZ {plz} not found for {person['name']}") + + return result + @app.post("/api/upload") async def upload_file(file: UploadFile = File(...)): global df_storage, plz_column_name diff --git a/heatmap-tool/frontend/src/App.tsx b/heatmap-tool/frontend/src/App.tsx index 9904821b..35ec0945 100644 --- a/heatmap-tool/frontend/src/App.tsx +++ b/heatmap-tool/frontend/src/App.tsx @@ -28,6 +28,18 @@ export interface HeatmapPoint { attributes_summary?: Record; } +export interface StaffLocation { + name: string; + plz: string; + lat: number; + lon: number; +} + +export interface StaffData { + sales: StaffLocation[]; + technicians: StaffLocation[]; +} + export type MapMode = 'points' | 'heatmap'; function App() { @@ -42,6 +54,22 @@ function App() { const [availableColumns, setAvailableColumns] = useState([]); const [tooltipColumns, setTooltipColumns] = useState([]); + const [staffData, setStaffData] = useState({ sales: [], technicians: [] }); + const [showSales, setShowSales] = useState(false); + const [showTechnicians, setShowTechnicians] = useState(false); + const [travelRadius, setTravelRadius] = useState(50); // In km + + useEffect(() => { + const fetchStaffLocations = async () => { + try { + const response = await axios.get('/heatmap/api/staff-locations'); + setStaffData(response.data); + } catch (err) { + console.error("Failed to fetch staff locations:", err); + } + }; + fetchStaffLocations(); + }, []); const handleUploadSuccess = (response: any) => { setError(null); @@ -153,6 +181,34 @@ function App() { Heatmap + +
+ + +
+ + {(showSales || showTechnicians) && ( +
+ + setTravelRadius(parseInt(e.target.value))} + style={{ width: '100%' }} + /> +
+ )} + )} diff --git a/heatmap-tool/frontend/src/components/MapDisplay.tsx b/heatmap-tool/frontend/src/components/MapDisplay.tsx index a14a7c5e..68ffa45f 100644 --- a/heatmap-tool/frontend/src/components/MapDisplay.tsx +++ b/heatmap-tool/frontend/src/components/MapDisplay.tsx @@ -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 = ({ heatmapData, radiusMultiplier, viewMode, tooltipColumns }) => { +const MapDisplay: React.FC = ({ + 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 = ({ 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 = () => ( {heatmapData.map((point, idx) => ( @@ -75,22 +115,68 @@ const MapDisplay: React.FC = ({ heatmapData, radiusMultiplier, /> ); - if (heatmapData.length === 0) { + const renderStaff = () => { + const elements: React.ReactNode[] = []; + + if (showSales) { + staffData.sales.forEach((person, idx) => { + elements.push( + + + Sales: {person.name} ({person.plz}) + + + + ); + }); + } + + if (showTechnicians) { + staffData.technicians.forEach((person, idx) => { + elements.push( + + + Tech: {person.name} ({person.plz}) + + + + ); + }); + } + + return elements; + }; + + const hasData = heatmapData.length > 0 || showSales || showTechnicians; + + if (!hasData) { return ( -
-

No data to display on the map.

-

Upload a file and apply filters to see the heatmap.

+
+
+

No data to display on the map.

+

Upload a file or enable staff layers to see something.

+
); } return ( - + - {viewMode === 'points' ? renderPoints() : renderHeatmap()} + {viewMode === 'points' && renderPoints()} + {viewMode === 'heatmap' && renderHeatmap()} + {renderStaff()} ); };