[31b88f42] Finalize staff locations and travel time logic

This commit is contained in:
2026-03-09 14:44:10 +00:00
parent 9afe4148ba
commit 2c90da3ba5
4 changed files with 185 additions and 28 deletions

View File

@@ -57,7 +57,13 @@ function App() {
const [staffData, setStaffData] = useState<StaffData>({ sales: [], technicians: [] });
const [showSales, setShowSales] = useState(false);
const [showTechnicians, setShowTechnicians] = useState(false);
const [travelRadius, setTravelRadius] = useState(50); // In km
const [travelTime, setTravelTime] = useState(60); // In minutes
const [averageSpeed, setAverageSpeed] = useState(80); // In km/h
const [isochrones, setIsochrones] = useState<Record<string, any>>({});
const [orsError, setOrsError] = useState<string | null>(null);
// Derived travel radius in km (fallback)
const travelRadius = (travelTime / 60) * averageSpeed;
useEffect(() => {
const fetchStaffLocations = async () => {
@@ -71,6 +77,56 @@ function App() {
fetchStaffLocations();
}, []);
// Fetch isochrones when settings change
useEffect(() => {
if (!showSales && !showTechnicians) return;
const fetchAllIsochrones = async () => {
const activeStaff = [
...(showSales ? staffData.sales : []),
...(showTechnicians ? staffData.technicians : [])
];
if (activeStaff.length === 0) return;
const newIsochrones: Record<string, any> = { ...isochrones };
let anyOrsError = false;
for (const person of activeStaff) {
// Skip if already fetched for this person and time
if (newIsochrones[person.name] && newIsochrones[person.name].minutes === travelTime) {
continue;
}
try {
const response = await axios.get('/heatmap/api/isochrones', {
params: { lat: person.lat, lon: person.lon, minutes: travelTime }
});
if (response.data.simulated || response.data.error) {
setOrsError("OpenRouteService API Key missing or invalid. Showing circular fallback.");
anyOrsError = true;
break;
}
newIsochrones[person.name] = {
data: response.data,
minutes: travelTime
};
} catch (err) {
console.error(`Failed to fetch isochrone for ${person.name}:`, err);
// Don't set anyOrsError here, just let this one person use the fallback
}
}
setIsochrones(newIsochrones);
if (!anyOrsError) setOrsError(null);
};
const timer = setTimeout(fetchAllIsochrones, 800); // Debounce
return () => clearTimeout(timer);
}, [travelTime, showSales, showTechnicians, staffData]);
const handleUploadSuccess = (response: any) => {
setError(null);
if (response.plz_column_needed) {
@@ -194,18 +250,44 @@ function App() {
</div>
{(showSales || showTechnicians) && (
<div className="radius-control" style={{ marginBottom: '20px' }}>
<label htmlFor="travel-radius">Travel Radius: {travelRadius} km</label>
<input
type="range"
id="travel-radius"
min="10"
max="200"
step="5"
value={travelRadius}
onChange={(e) => setTravelRadius(parseInt(e.target.value))}
style={{ width: '100%' }}
/>
<div className="radius-control" style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#444', borderRadius: '4px' }}>
<div style={{ marginBottom: '10px' }}>
<label htmlFor="travel-time">Travel Time: {travelTime} min (max 60)</label>
<input
type="range"
id="travel-time"
min="15"
max="60"
step="5"
value={travelTime}
</div>
{orsError ? (
<>
<div style={{ marginBottom: '10px' }}>
<label htmlFor="avg-speed">Avg. Speed: {averageSpeed} km/h</label>
<input
type="range"
id="avg-speed"
min="30"
max="120"
step="5"
value={averageSpeed}
onChange={(e) => setAverageSpeed(parseInt(e.target.value))}
style={{ width: '100%' }}
/>
</div>
<div style={{ fontSize: '0.8em', color: '#ffcc00', marginBottom: '5px' }}>
{orsError}
</div>
<div style={{ fontSize: '0.9em', color: '#ccc', textAlign: 'center', borderTop: '1px solid #666', paddingTop: '5px' }}>
Resulting Radius: <strong>{travelRadius.toFixed(1)} km</strong>
</div>
</>
) : (
<div style={{ fontSize: '0.8em', color: '#4CAF50' }}>
Using real road data (OpenRouteService)
</div>
)}
</div>
)}
@@ -240,6 +322,7 @@ function App() {
showSales={showSales}
showTechnicians={showTechnicians}
travelRadius={travelRadius}
isochrones={isochrones}
/>
)}
</div>