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.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> </head>
<body> <body>
<div id="root"></div> <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 { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; 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 { Calendar, MapPin, User, Car, Clock } from "lucide-react";
import { format, parseISO, isValid } from "date-fns"; import { format, parseISO, isValid } from "date-fns";
import { it } from "date-fns/locale"; 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"; type Location = "roccapiemonte" | "milano" | "roma";
@ -16,12 +26,11 @@ type MobileSite = {
id: string; id: string;
name: string; name: string;
address: string; address: string;
city: string; serviceTypeId: string | null;
serviceTypeId: string; serviceTypeName: string | null;
serviceTypeName: string;
location: Location; location: Location;
latitude: number | null; latitude: string | null;
longitude: number | null; longitude: string | null;
}; };
type AvailableGuard = { type AvailableGuard = {
@ -77,6 +86,23 @@ export default function PlanningMobile() {
roma: "bg-purple-500", 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 ( return (
<div className="h-full flex flex-col p-6 space-y-6"> <div className="h-full flex flex-col p-6 space-y-6">
{/* Header */} {/* Header */}
@ -154,15 +180,50 @@ export default function PlanningMobile() {
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]} {mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-lg"> <CardContent className="flex-1 p-0 overflow-hidden">
<div className="text-center space-y-2"> {sitesWithCoordinates.length > 0 ? (
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" /> <MapContainer
<p className="text-sm text-muted-foreground"> key={selectedLocation}
Integrazione mappa in sviluppo center={locationCenters[selectedLocation]}
<br /> zoom={12}
(Leaflet/Google Maps) className="h-full w-full min-h-[400px]"
</p> data-testid="map-container"
</div> >
<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">
Nessun sito con coordinate GPS disponibile
<br />
<span className="text-xs">Aggiungi latitudine e longitudine ai siti per visualizzarli sulla mappa</span>
</p>
</div>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
@ -192,18 +253,20 @@ export default function PlanningMobile() {
<h4 className="font-semibold">{site.name}</h4> <h4 className="font-semibold">{site.name}</h4>
<p className="text-sm text-muted-foreground flex items-center gap-1"> <p className="text-sm text-muted-foreground flex items-center gap-1">
<MapPin className="h-3 w-3" /> <MapPin className="h-3 w-3" />
{site.address}, {site.city} {site.address}
</p> </p>
</div> </div>
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}> <Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
{locationLabels[site.location]} {locationLabels[site.location]}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-2 text-sm"> {site.serviceTypeName && (
<Badge variant="outline" data-testid={`badge-service-${site.id}`}> <div className="flex items-center gap-2 text-sm">
{site.serviceTypeName} <Badge variant="outline" data-testid={`badge-service-${site.id}`}>
</Badge> {site.serviceTypeName}
</div> </Badge>
</div>
)}
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}> <Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
Assegna Guardia Assegna Guardia

View File

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