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.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>
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
<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" />
|
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Integrazione mappa in sviluppo
|
Nessun sito con coordinate GPS disponibile
|
||||||
<br />
|
<br />
|
||||||
(Leaflet/Google Maps)
|
<span className="text-xs">Aggiungi latitudine e longitudine ai siti per visualizzarli sulla mappa</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
{site.serviceTypeName && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
|
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
|
||||||
{site.serviceTypeName}
|
{site.serviceTypeName}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</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
|
||||||
|
|||||||
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user