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
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 35023
|
||||
externalPort = 6000
|
||||
|
||||
[[ports]]
|
||||
localPort = 41295
|
||||
externalPort = 5173
|
||||
|
||||
@ -6,7 +6,7 @@ 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, 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 {
|
||||
DndContext,
|
||||
@ -169,6 +169,17 @@ export default function PlanningMobile() {
|
||||
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
|
||||
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
|
||||
const savePatrolRouteMutation = useMutation({
|
||||
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
|
||||
const handleSavePatrolRoute = () => {
|
||||
if (!selectedGuard) {
|
||||
@ -645,7 +725,7 @@ export default function PlanningMobile() {
|
||||
<CardDescription>
|
||||
{patrolRoute.length} tappe programmate per il turno del {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
|
||||
<br />
|
||||
<span className="text-xs">💡 Trascina le tappe per riordinarle</span>
|
||||
<span className="text-xs">Trascina le tappe per riordinarle</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
@ -670,12 +750,22 @@ export default function PlanningMobile() {
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<div className="flex gap-2 pt-2 flex-wrap">
|
||||
<Button
|
||||
onClick={handleSavePatrolRoute}
|
||||
disabled={savePatrolRouteMutation.isPending}
|
||||
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
|
||||
variant="outline"
|
||||
@ -1096,6 +1186,67 @@ export default function PlanningMobile() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user