Integrate map functionality for planning mobile view

Add Leaflet map integration to the planning mobile view, displaying sites with coordinates and filtering guards with driver licenses.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/TFybNy5
This commit is contained in:
marco370 2025-10-23 10:46:54 +00:00
parent 0cfa154e61
commit 7431145ee3
3 changed files with 89 additions and 23 deletions

View File

@ -18,6 +18,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head>
<body>
<div id="root"></div>

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@ -9,6 +9,16 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Calendar, MapPin, User, Car, Clock } from "lucide-react";
import { format, parseISO, isValid } from "date-fns";
import { it } from "date-fns/locale";
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
// Fix Leaflet default icon issue with Webpack
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
});
type Location = "roccapiemonte" | "milano" | "roma";
@ -16,12 +26,11 @@ type MobileSite = {
id: string;
name: string;
address: string;
city: string;
serviceTypeId: string;
serviceTypeName: string;
serviceTypeId: string | null;
serviceTypeName: string | null;
location: Location;
latitude: number | null;
longitude: number | null;
latitude: string | null;
longitude: string | null;
};
type AvailableGuard = {
@ -77,6 +86,23 @@ export default function PlanningMobile() {
roma: "bg-purple-500",
};
// Coordinate di default per centrare la mappa sulla location selezionata
const locationCenters: Record<Location, [number, number]> = {
roccapiemonte: [40.8167, 14.6167], // Roccapiemonte, Salerno
milano: [45.4642, 9.1900], // Milano
roma: [41.9028, 12.4964], // Roma
};
// Filtra siti con coordinate valide per la mappa
const sitesWithCoordinates = useMemo(() => {
return mobileSites?.filter((site) =>
site.latitude !== null &&
site.longitude !== null &&
!isNaN(parseFloat(site.latitude)) &&
!isNaN(parseFloat(site.longitude))
) || [];
}, [mobileSites]);
return (
<div className="h-full flex flex-col p-6 space-y-6">
{/* Header */}
@ -154,15 +180,50 @@ export default function PlanningMobile() {
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-lg">
<div className="text-center space-y-2">
<CardContent className="flex-1 p-0 overflow-hidden">
{sitesWithCoordinates.length > 0 ? (
<MapContainer
key={selectedLocation}
center={locationCenters[selectedLocation]}
zoom={12}
className="h-full w-full min-h-[400px]"
data-testid="map-container"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{sitesWithCoordinates.map((site) => (
<Marker
key={site.id}
position={[parseFloat(site.latitude!), parseFloat(site.longitude!)]}
>
<Popup>
<div className="space-y-1">
<h4 className="font-semibold text-sm">{site.name}</h4>
<p className="text-xs text-muted-foreground">{site.address}</p>
{site.serviceTypeName && (
<Badge variant="outline" className="text-xs">
{site.serviceTypeName}
</Badge>
)}
</div>
</Popup>
</Marker>
))}
</MapContainer>
) : (
<div className="h-full flex items-center justify-center bg-muted/20">
<div className="text-center space-y-2 p-6">
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Integrazione mappa in sviluppo
Nessun sito con coordinate GPS disponibile
<br />
(Leaflet/Google Maps)
<span className="text-xs">Aggiungi latitudine e longitudine ai siti per visualizzarli sulla mappa</span>
</p>
</div>
</div>
)}
</CardContent>
</Card>
@ -192,18 +253,20 @@ export default function PlanningMobile() {
<h4 className="font-semibold">{site.name}</h4>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<MapPin className="h-3 w-3" />
{site.address}, {site.city}
{site.address}
</p>
</div>
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
{locationLabels[site.location]}
</Badge>
</div>
{site.serviceTypeName && (
<div className="flex items-center gap-2 text-sm">
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
{site.serviceTypeName}
</Badge>
</div>
)}
<div className="flex gap-2 pt-2">
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
Assegna Guardia

View File

@ -3191,7 +3191,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
id: sites.id,
name: sites.name,
address: sites.address,
city: sites.city,
serviceTypeId: sites.serviceTypeId,
serviceTypeName: serviceTypes.label,
location: sites.location,
@ -3242,7 +3241,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
.where(
and(
eq(guards.location, location as "roccapiemonte" | "milano" | "roma"),
eq(guards.isActive, true),
eq(guards.hasDriverLicense, true)
)
)