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:
marco370 2025-10-25 08:59:53 +00:00
parent 5c22ec14f1
commit dd84ddb35b
2 changed files with 155 additions and 8 deletions

View File

@ -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

View File

@ -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>
); );
} }