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:
parent
0cfa154e61
commit
7431145ee3
@ -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>
|
||||
|
||||
@ -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='© <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
|
||||
|
||||
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user