diff --git a/.replit b/.replit index 75d108a..9b52848 100644 --- a/.replit +++ b/.replit @@ -21,7 +21,7 @@ externalPort = 3001 [[ports]] localPort = 41295 -externalPort = 5173 +externalPort = 6000 [[ports]] localPort = 41343 @@ -43,6 +43,10 @@ externalPort = 5000 localPort = 43267 externalPort = 3003 +[[ports]] +localPort = 45047 +externalPort = 5173 + [env] PORT = "5000" diff --git a/client/src/pages/planning-mobile.tsx b/client/src/pages/planning-mobile.tsx index 95f7f50..0895ca3 100644 --- a/client/src/pages/planning-mobile.tsx +++ b/client/src/pages/planning-mobile.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useRef, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -6,11 +6,12 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Calendar, MapPin, User, Car, Clock } from "lucide-react"; +import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered } 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 { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'; import L from 'leaflet'; +import { useToast } from "@/hooks/use-toast"; // Fix Leaflet default icon issue with Webpack delete (L.Icon.Default.prototype as any)._getIconUrl; @@ -43,10 +44,29 @@ type AvailableGuard = { availableHours: number; }; +// Componente per controllare la mappa da fuori +function MapController({ center, zoom }: { center: [number, number] | null; zoom: number }) { + const map = useMap(); + + useEffect(() => { + if (center) { + map.flyTo(center, zoom, { + duration: 1.5, + }); + } + }, [center, zoom, map]); + + return null; +} + export default function PlanningMobile() { + const { toast } = useToast(); const [selectedDate, setSelectedDate] = useState(format(new Date(), "yyyy-MM-dd")); const [selectedLocation, setSelectedLocation] = useState("roccapiemonte"); const [selectedGuardId, setSelectedGuardId] = useState("all"); + const [mapCenter, setMapCenter] = useState<[number, number] | null>(null); + const [mapZoom, setMapZoom] = useState(12); + const [patrolRoute, setPatrolRoute] = useState([]); // Query siti mobile per location const { data: mobileSites, isLoading: sitesLoading } = useQuery({ @@ -103,6 +123,119 @@ export default function PlanningMobile() { ) || []; }, [mobileSites]); + // Trova guardia selezionata + const selectedGuard = useMemo(() => { + if (selectedGuardId === "all") return null; + return availableGuards?.find(g => g.id === selectedGuardId) || null; + }, [selectedGuardId, availableGuards]); + + // Funzione per zoomare su un sito + const handleZoomToSite = (site: MobileSite) => { + if (site.latitude && site.longitude) { + setMapCenter([parseFloat(site.latitude), parseFloat(site.longitude)]); + setMapZoom(16); // Zoom ravvicinato + toast({ + title: "Mappa centrata", + description: `Visualizzazione di ${site.name}`, + }); + } else { + toast({ + title: "Coordinate mancanti", + description: "Questo sito non ha coordinate GPS", + variant: "destructive", + }); + } + }; + + // Funzione per assegnare guardia a un sito + const handleAssignGuard = (site: MobileSite) => { + if (!selectedGuard) { + toast({ + title: "Guardia non selezionata", + description: "Seleziona una guardia dal filtro in alto", + variant: "destructive", + }); + return; + } + + toast({ + title: "Guardia assegnata", + description: `${site.name} assegnato a ${selectedGuard.firstName} ${selectedGuard.lastName}`, + }); + }; + + // Funzione per aggiungere sito alla patrol route + const handleAddToRoute = (site: MobileSite) => { + if (!selectedGuard) { + toast({ + title: "Seleziona una guardia", + description: "Prima seleziona una guardia per creare il turno pattuglia", + variant: "destructive", + }); + return; + } + + // Controlla se il sito è già nella route + const alreadyInRoute = patrolRoute.some(s => s.id === site.id); + if (alreadyInRoute) { + toast({ + title: "Sito già in sequenza", + description: `${site.name} è già nella sequenza di pattuglia`, + variant: "destructive", + }); + return; + } + + setPatrolRoute([...patrolRoute, site]); + toast({ + title: "Sito aggiunto alla sequenza", + description: `${site.name} - Tappa ${patrolRoute.length + 1} di ${patrolRoute.length + 1}`, + }); + }; + + // Funzione per rimuovere sito dalla route + const handleRemoveFromRoute = (siteId: string) => { + setPatrolRoute(patrolRoute.filter(s => s.id !== siteId)); + toast({ + title: "Sito rimosso", + description: "Sito rimosso dalla sequenza di pattuglia", + }); + }; + + // Funzione per salvare il turno pattuglia + const handleSavePatrolRoute = () => { + if (!selectedGuard) { + toast({ + title: "Guardia non selezionata", + description: "Seleziona una guardia per salvare il turno", + variant: "destructive", + }); + return; + } + + if (patrolRoute.length === 0) { + toast({ + title: "Nessun sito nella sequenza", + description: "Aggiungi almeno un sito alla sequenza di pattuglia", + variant: "destructive", + }); + return; + } + + // TODO: Implementare chiamata API per salvare turno + toast({ + title: "Turno pattuglia creato", + description: `${patrolRoute.length} tappe assegnate a ${selectedGuard.firstName} ${selectedGuard.lastName}`, + }); + + setPatrolRoute([]); + }; + + // Reset patrol route quando cambia guardia o location + useEffect(() => { + setPatrolRoute([]); + }, [selectedGuardId, selectedLocation]); + return (
{/* Header */} @@ -167,6 +300,61 @@ export default function PlanningMobile() { + {/* Sequenza Pattuglia */} + {selectedGuard && patrolRoute.length > 0 && ( + + + + + Sequenza Pattuglia - {selectedGuard.firstName} {selectedGuard.lastName} + + + {patrolRoute.length} tappe programmate per il turno del {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })} + + + +
+ {patrolRoute.map((site, index) => ( +
+ + {index + 1} + + {site.name} + +
+ ))} +
+
+ + +
+
+
+ )} + {/* Grid: Mappa + Siti */}
{/* Mappa Siti */} @@ -189,28 +377,42 @@ export default function PlanningMobile() { className="h-full w-full min-h-[400px]" data-testid="map-container" > + - {sitesWithCoordinates.map((site) => ( - - -
-

{site.name}

-

{site.address}

- {site.serviceTypeName && ( - - {site.serviceTypeName} - - )} -
-
-
- ))} + {sitesWithCoordinates.map((site) => { + const isInRoute = patrolRoute.some(s => s.id === site.id); + const routeIndex = patrolRoute.findIndex(s => s.id === site.id); + + return ( + handleAddToRoute(site), + }} + > + +
+

{site.name}

+

{site.address}

+ {site.serviceTypeName && ( + + {site.serviceTypeName} + + )} + {isInRoute && ( + + Tappa {routeIndex + 1} + + )} +
+
+
+ ); + })} ) : (
@@ -242,41 +444,61 @@ export default function PlanningMobile() { {sitesLoading ? (

Caricamento...

) : mobileSites && mobileSites.length > 0 ? ( - mobileSites.map((site) => ( -
-
-
-

{site.name}

-

- - {site.address} -

-
- - {locationLabels[site.location]} - -
- {site.serviceTypeName && ( -
- - {site.serviceTypeName} + mobileSites.map((site) => { + const isInRoute = patrolRoute.some(s => s.id === site.id); + const routeIndex = patrolRoute.findIndex(s => s.id === site.id); + + return ( +
+
+
+

{site.name}

+

+ + {site.address} +

+
+ + {locationLabels[site.location]}
- )} -
- - + {site.serviceTypeName && ( +
+ + {site.serviceTypeName} + + {isInRoute && ( + + Tappa {routeIndex + 1} + + )} +
+ )} +
+ + +
-
- )) + ); + }) ) : (