Add route optimization and display results in mobile planning
Integrate a new API endpoint for route optimization, update the UI to include Sparkles icon, and implement state management for optimization results including total distance and estimated time. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/N0pfy8w
This commit is contained in:
parent
5c22ec14f1
commit
dd84ddb35b
4
.replit
4
.replit
@ -19,10 +19,6 @@ externalPort = 80
|
|||||||
localPort = 33035
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 35023
|
|
||||||
externalPort = 6000
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41295
|
localPort = 41295
|
||||||
externalPort = 5173
|
externalPort = 5173
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered, Copy, GripVertical } from "lucide-react";
|
import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered, Copy, GripVertical, Sparkles } from "lucide-react";
|
||||||
import { format, parseISO, isValid, addDays } from "date-fns";
|
import { format, parseISO, isValid, addDays } from "date-fns";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@ -169,6 +169,17 @@ export default function PlanningMobile() {
|
|||||||
selectedDuplicateGuardId: "",
|
selectedDuplicateGuardId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// State per dialog risultati ottimizzazione
|
||||||
|
const [optimizationResults, setOptimizationResults] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
totalDistanceKm: string;
|
||||||
|
estimatedTime: string;
|
||||||
|
}>({
|
||||||
|
isOpen: false,
|
||||||
|
totalDistanceKm: "",
|
||||||
|
estimatedTime: "",
|
||||||
|
});
|
||||||
|
|
||||||
// Ref per scroll alla sezione sequenze pattuglia
|
// Ref per scroll alla sezione sequenze pattuglia
|
||||||
const patrolSequencesRef = useRef<HTMLDivElement>(null);
|
const patrolSequencesRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -471,6 +482,36 @@ export default function PlanningMobile() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mutation per ottimizzare percorso
|
||||||
|
const optimizeRouteMutation = useMutation({
|
||||||
|
mutationFn: async (coordinates: { lat: string; lon: string; id: string; name: string }[]) => {
|
||||||
|
const response = await apiRequest("POST", "/api/optimize-route", { coordinates });
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: (data: any) => {
|
||||||
|
// Riordina le tappe secondo l'ordine ottimizzato
|
||||||
|
const optimizedStops = data.optimizedRoute.map((coord: any) =>
|
||||||
|
patrolRoute.find(site => site.id === coord.id)
|
||||||
|
).filter((site: any) => site !== undefined) as MobileSite[];
|
||||||
|
|
||||||
|
setPatrolRoute(optimizedStops);
|
||||||
|
|
||||||
|
// Mostra dialog con risultati
|
||||||
|
setOptimizationResults({
|
||||||
|
isOpen: true,
|
||||||
|
totalDistanceKm: data.totalDistanceKm,
|
||||||
|
estimatedTime: data.estimatedTimeFormatted,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Errore ottimizzazione",
|
||||||
|
description: error.message || "Impossibile ottimizzare il percorso",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mutation per salvare patrol route
|
// Mutation per salvare patrol route
|
||||||
const savePatrolRouteMutation = useMutation({
|
const savePatrolRouteMutation = useMutation({
|
||||||
mutationFn: async ({ data, existingRouteId }: { data: any; existingRouteId?: string }) => {
|
mutationFn: async ({ data, existingRouteId }: { data: any; existingRouteId?: string }) => {
|
||||||
@ -501,6 +542,45 @@ export default function PlanningMobile() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Funzione per ottimizzare il percorso
|
||||||
|
const handleOptimizeRoute = () => {
|
||||||
|
// Verifica che ci siano almeno 2 tappe
|
||||||
|
if (patrolRoute.length < 2) {
|
||||||
|
toast({
|
||||||
|
title: "Tappe insufficienti",
|
||||||
|
description: "Servono almeno 2 tappe per ottimizzare il percorso",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che tutte le tappe abbiano coordinate GPS
|
||||||
|
const sitesWithCoords = patrolRoute.filter(site =>
|
||||||
|
site.latitude && site.longitude &&
|
||||||
|
!isNaN(parseFloat(site.latitude)) &&
|
||||||
|
!isNaN(parseFloat(site.longitude))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sitesWithCoords.length !== patrolRoute.length) {
|
||||||
|
toast({
|
||||||
|
title: "Coordinate GPS mancanti",
|
||||||
|
description: `${patrolRoute.length - sitesWithCoords.length} tappe non hanno coordinate GPS valide`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepara dati per ottimizzazione
|
||||||
|
const coordinates = sitesWithCoords.map(site => ({
|
||||||
|
lat: site.latitude!,
|
||||||
|
lon: site.longitude!,
|
||||||
|
id: site.id,
|
||||||
|
name: site.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
optimizeRouteMutation.mutate(coordinates);
|
||||||
|
};
|
||||||
|
|
||||||
// Funzione per salvare il turno pattuglia
|
// Funzione per salvare il turno pattuglia
|
||||||
const handleSavePatrolRoute = () => {
|
const handleSavePatrolRoute = () => {
|
||||||
if (!selectedGuard) {
|
if (!selectedGuard) {
|
||||||
@ -645,7 +725,7 @@ export default function PlanningMobile() {
|
|||||||
<CardDescription>
|
<CardDescription>
|
||||||
{patrolRoute.length} tappe programmate per il turno del {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
|
{patrolRoute.length} tappe programmate per il turno del {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
|
||||||
<br />
|
<br />
|
||||||
<span className="text-xs">💡 Trascina le tappe per riordinarle</span>
|
<span className="text-xs">Trascina le tappe per riordinarle</span>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
@ -670,12 +750,22 @@ export default function PlanningMobile() {
|
|||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSavePatrolRoute}
|
onClick={handleSavePatrolRoute}
|
||||||
|
disabled={savePatrolRouteMutation.isPending}
|
||||||
data-testid="button-save-patrol-route"
|
data-testid="button-save-patrol-route"
|
||||||
>
|
>
|
||||||
Salva Turno Pattuglia
|
{savePatrolRouteMutation.isPending ? "Salvataggio..." : "Salva Turno Pattuglia"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleOptimizeRoute}
|
||||||
|
disabled={optimizeRouteMutation.isPending || patrolRoute.length < 2}
|
||||||
|
data-testid="button-optimize-route"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4 mr-2" />
|
||||||
|
{optimizeRouteMutation.isPending ? "Ottimizzazione..." : "Ottimizza Sequenza"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -1096,6 +1186,67 @@ export default function PlanningMobile() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Dialog Risultati Ottimizzazione */}
|
||||||
|
<Dialog open={optimizationResults.isOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setOptimizationResults({
|
||||||
|
isOpen: false,
|
||||||
|
totalDistanceKm: "",
|
||||||
|
estimatedTime: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Sparkles className="h-5 w-5" />
|
||||||
|
Percorso Ottimizzato
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Il percorso è stato ottimizzato per ridurre i chilometri percorsi
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-muted-foreground">Distanza Totale</Label>
|
||||||
|
<div className="text-2xl font-bold">{optimizationResults.totalDistanceKm} km</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Partenza e ritorno dalla prima tappa
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-muted-foreground">Tempo Stimato</Label>
|
||||||
|
<div className="text-2xl font-bold">{optimizationResults.estimatedTime}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Tempo di percorrenza stimato
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p className="text-sm">
|
||||||
|
Le tappe sono state riordinate per minimizzare la distanza percorsa.
|
||||||
|
Salva il turno pattuglia per confermare le modifiche.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOptimizationResults({
|
||||||
|
isOpen: false,
|
||||||
|
totalDistanceKm: "",
|
||||||
|
estimatedTime: "",
|
||||||
|
})}
|
||||||
|
data-testid="button-close-optimization-results"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user