diff --git a/.replit b/.replit index 559073f..6b5c189 100644 --- a/.replit +++ b/.replit @@ -39,10 +39,6 @@ externalPort = 3002 localPort = 42187 externalPort = 6800 -[[ports]] -localPort = 42277 -externalPort = 6000 - [[ports]] localPort = 43169 externalPort = 5000 diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx index 2d2d472..9f69fa0 100644 --- a/client/src/pages/general-planning.tsx +++ b/client/src/pages/general-planning.tsx @@ -1056,35 +1056,37 @@ export default function GeneralPlanning() { Copia Turno Settimanale - -

- Vuoi copiare tutti i turni della settimana corrente nella settimana successiva? -

- {planningData && ( -
-
- Settimana corrente: - - {format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "} - {format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })} - + +
+

+ Vuoi copiare tutti i turni della settimana corrente nella settimana successiva? +

+ {planningData && ( +
+
+ Settimana corrente: + + {format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "} + {format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })} + +
+
+ Verrà copiata in: + + {format(addWeeks(new Date(planningData.weekStart), 1), "dd MMM", { locale: it })} -{" "} + {format(addWeeks(new Date(planningData.weekEnd), 1), "dd MMM yyyy", { locale: it })} + +
+
+ Sede: + {formatLocation(selectedLocation)} +
-
- Verrà copiata in: - - {format(addWeeks(new Date(planningData.weekStart), 1), "dd MMM", { locale: it })} -{" "} - {format(addWeeks(new Date(planningData.weekEnd), 1), "dd MMM yyyy", { locale: it })} - -
-
- Sede: - {formatLocation(selectedLocation)} -
-
- )} -

- Tutti i turni e le assegnazioni guardie verranno duplicati con le stesse caratteristiche (orari, dotazioni, veicoli). -

+ )} +

+ Tutti i turni e le assegnazioni guardie verranno duplicati con le stesse caratteristiche (orari, dotazioni, veicoli). +

+
diff --git a/client/src/pages/planning-mobile.tsx b/client/src/pages/planning-mobile.tsx index 851336a..6f5da0a 100644 --- a/client/src/pages/planning-mobile.tsx +++ b/client/src/pages/planning-mobile.tsx @@ -6,8 +6,16 @@ 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, Navigation, ListOrdered } from "lucide-react"; -import { format, parseISO, isValid } from "date-fns"; +import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered, Copy } from "lucide-react"; +import { format, parseISO, isValid, addDays } from "date-fns"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { it } from "date-fns/locale"; import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'; import L from 'leaflet'; @@ -86,6 +94,19 @@ export default function PlanningMobile() { const [mapCenter, setMapCenter] = useState<[number, number] | null>(null); const [mapZoom, setMapZoom] = useState(12); const [patrolRoute, setPatrolRoute] = useState([]); + + // State per dialog duplicazione sequenza + const [duplicateDialog, setDuplicateDialog] = useState<{ + isOpen: boolean; + sourceRoute: any | null; + targetDate: string; + selectedDuplicateGuardId: string; + }>({ + isOpen: false, + sourceRoute: null, + targetDate: "", + selectedDuplicateGuardId: "", + }); // Query siti mobile per location const { data: mobileSites, isLoading: sitesLoading } = useQuery({ @@ -131,6 +152,55 @@ export default function PlanningMobile() { enabled: !!selectedDate && !!selectedLocation, }); + // Mutation per duplicare sequenza pattuglia + const duplicatePatrolRouteMutation = useMutation({ + mutationFn: async (data: { sourceRouteId: string; targetDate: string; guardId: string }) => { + return apiRequest("POST", "/api/patrol-routes/duplicate", data); + }, + onSuccess: async (response: any) => { + const data = await response.json(); + + const actionLabel = data.action === "updated" ? "modificata" : "duplicata"; + + toast({ + title: `Sequenza ${actionLabel}!`, + description: data.message, + }); + + // Invalida cache e chiudi dialog + await queryClient.invalidateQueries({ queryKey: ["/api/patrol-routes"] }); + setDuplicateDialog({ + isOpen: false, + sourceRoute: null, + targetDate: "", + selectedDuplicateGuardId: "", + }); + }, + onError: (error: any) => { + let errorMessage = "Impossibile duplicare la sequenza"; + + if (error.message) { + const match = error.message.match(/^(\d+):\s*(.+)$/); + if (match) { + try { + const parsed = JSON.parse(match[2]); + errorMessage = parsed.message || errorMessage; + } catch { + errorMessage = match[2]; + } + } else { + errorMessage = error.message; + } + } + + toast({ + title: "Errore Duplicazione", + description: errorMessage, + variant: "destructive", + }); + }, + }); + const locationLabels: Record = { roccapiemonte: "Roccapiemonte", milano: "Milano", @@ -201,6 +271,35 @@ export default function PlanningMobile() { }); }; + // Funzione per aprire dialog duplicazione sequenza + const handleOpenDuplicateDialog = (route: any) => { + const nextDay = format(addDays(parseISO(selectedDate), 1), "yyyy-MM-dd"); + setDuplicateDialog({ + isOpen: true, + sourceRoute: route, + targetDate: nextDay, // Default = giorno successivo + selectedDuplicateGuardId: route.guardId, // Pre-compilato con guardia attuale + }); + }; + + // Handler submit dialog duplicazione + const handleSubmitDuplicate = () => { + if (!duplicateDialog.sourceRoute || !duplicateDialog.targetDate || !duplicateDialog.selectedDuplicateGuardId) { + toast({ + title: "Campi mancanti", + description: "Compila tutti i campi obbligatori", + variant: "destructive", + }); + return; + } + + duplicatePatrolRouteMutation.mutate({ + sourceRouteId: duplicateDialog.sourceRoute.id, + targetDate: duplicateDialog.targetDate, + guardId: duplicateDialog.selectedDuplicateGuardId, + }); + }; + // Funzione per aggiungere sito alla patrol route const handleAddToRoute = (site: MobileSite) => { if (!selectedGuard) { @@ -667,6 +766,193 @@ export default function PlanningMobile() {
+ + {/* Sequenze Pattuglia del Giorno */} + + + + + Sequenze Pattuglia - {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })} + + + Sequenze programmate per la data selezionata + + + + {existingPatrolRoutes && existingPatrolRoutes.length > 0 ? ( +
+ {existingPatrolRoutes.map((route) => { + const guard = availableGuards?.find(g => g.id === route.guardId); + return ( +
+
+
+
+ + + {guard ? `${guard.firstName} ${guard.lastName}` : "Guardia sconosciuta"} + + {guard && ( + + #{guard.badgeNumber} + + )} +
+
+
+ + {route.startTime} - {route.endTime} +
+
+ + {route.stops?.length || 0} {route.stops?.length === 1 ? "tappa" : "tappe"} +
+
+ {route.stops && route.stops.length > 0 && ( +
+ Sequenza: + {route.stops.map((stop: any, idx: number) => ( + + {stop.siteName}{idx < route.stops.length - 1 ? " → " : ""} + + ))} +
+ )} +
+ +
+
+ ); + })} +
+ ) : ( +
+ +

+ Nessuna sequenza pattuglia pianificata per {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })} +

+
+ )} +
+
+ + {/* Dialog Duplica Sequenza */} + { + if (!open) { + setDuplicateDialog({ + isOpen: false, + sourceRoute: null, + targetDate: "", + selectedDuplicateGuardId: "", + }); + } + }}> + + + + + Duplica/Modifica Sequenza Pattuglia + + + Copia la sequenza in un'altra data o modifica la guardia assegnata + + + +
+ {/* Data Sorgente (readonly) */} + {duplicateDialog.sourceRoute && ( +
+ +
+
+ Data: + + {format(parseISO(duplicateDialog.sourceRoute.scheduledDate), "dd/MM/yyyy", { locale: it })} + +
+
+ Tappe: + {duplicateDialog.sourceRoute.stops?.length || 0} +
+
+
+ )} + + {/* Data Target */} +
+ + setDuplicateDialog({ ...duplicateDialog, targetDate: e.target.value })} + data-testid="input-target-date" + /> +

+ {duplicateDialog.sourceRoute && duplicateDialog.targetDate && + format(parseISO(duplicateDialog.sourceRoute.scheduledDate), "yyyy-MM-dd") === duplicateDialog.targetDate + ? "⚠️ Stessa data: verrà modificata la guardia della sequenza esistente" + : "✓ Data diversa: verrà creata una nuova sequenza con tutte le tappe" + } +

+
+ + {/* Selezione Guardia */} +
+ + +
+
+ + + + + +
+
); }