Add GPS coordinate lookup and display for site locations

Integrate OpenStreetMap Nominatim API for geocoding addresses to latitude and longitude, enabling GPS coordinate storage and display for sites. Update User-Agent for Nominatim requests.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/TFybNy5
This commit is contained in:
marco370 2025-10-23 11:06:56 +00:00
parent db860125fc
commit 0c702f4dbf
2 changed files with 211 additions and 1 deletions

View File

@ -28,6 +28,8 @@ export default function Sites() {
const { toast } = useToast(); const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingSite, setEditingSite] = useState<Site | null>(null); const [editingSite, setEditingSite] = useState<Site | null>(null);
const [isGeocoding, setIsGeocoding] = useState(false);
const [isGeocodingEdit, setIsGeocodingEdit] = useState(false);
const { data: sites, isLoading } = useQuery<Site[]>({ const { data: sites, isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"], queryKey: ["/api/sites"],
@ -57,6 +59,8 @@ export default function Sites() {
contractEndDate: undefined, contractEndDate: undefined,
serviceStartTime: "", serviceStartTime: "",
serviceEndTime: "", serviceEndTime: "",
latitude: undefined,
longitude: undefined,
isActive: true, isActive: true,
}, },
}); });
@ -77,6 +81,8 @@ export default function Sites() {
contractEndDate: undefined, contractEndDate: undefined,
serviceStartTime: "", serviceStartTime: "",
serviceEndTime: "", serviceEndTime: "",
latitude: undefined,
longitude: undefined,
isActive: true, isActive: true,
}, },
}); });
@ -125,6 +131,80 @@ export default function Sites() {
}, },
}); });
const handleGeocode = async () => {
const address = form.getValues("address");
if (!address) {
toast({
title: "Indirizzo mancante",
description: "Inserisci un indirizzo prima di cercare le coordinate",
variant: "destructive",
});
return;
}
setIsGeocoding(true);
try {
const result: any = await apiRequest(
"POST",
"/api/geocode",
{ address }
);
form.setValue("latitude", result.latitude);
form.setValue("longitude", result.longitude);
toast({
title: "Coordinate trovate",
description: `Indirizzo: ${result.displayName}`,
});
} catch (error: any) {
toast({
title: "Errore geocodifica",
description: error.message || "Impossibile trovare le coordinate per questo indirizzo",
variant: "destructive",
});
} finally {
setIsGeocoding(false);
}
};
const handleGeocodeEdit = async () => {
const address = editForm.getValues("address");
if (!address) {
toast({
title: "Indirizzo mancante",
description: "Inserisci un indirizzo prima di cercare le coordinate",
variant: "destructive",
});
return;
}
setIsGeocodingEdit(true);
try {
const result: any = await apiRequest(
"POST",
"/api/geocode",
{ address }
);
editForm.setValue("latitude", result.latitude);
editForm.setValue("longitude", result.longitude);
toast({
title: "Coordinate trovate",
description: `Indirizzo: ${result.displayName}`,
});
} catch (error: any) {
toast({
title: "Errore geocodifica",
description: error.message || "Impossibile trovare le coordinate per questo indirizzo",
variant: "destructive",
});
} finally {
setIsGeocodingEdit(false);
}
};
const onSubmit = (data: InsertSite) => { const onSubmit = (data: InsertSite) => {
createMutation.mutate(data); createMutation.mutate(data);
}; };
@ -151,6 +231,8 @@ export default function Sites() {
contractEndDate: site.contractEndDate || undefined, contractEndDate: site.contractEndDate || undefined,
serviceStartTime: site.serviceStartTime || "", serviceStartTime: site.serviceStartTime || "",
serviceEndTime: site.serviceEndTime || "", serviceEndTime: site.serviceEndTime || "",
latitude: site.latitude || undefined,
longitude: site.longitude || undefined,
isActive: site.isActive, isActive: site.isActive,
}); });
}; };
@ -234,6 +316,69 @@ export default function Sites() {
)} )}
/> />
<div className="border rounded-lg p-4 space-y-4 bg-muted/50">
<div className="flex items-center justify-between">
<p className="text-sm font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" />
Coordinate GPS (per mappa)
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleGeocode}
disabled={isGeocoding || !form.watch("address")}
data-testid="button-geocode"
>
{isGeocoding ? "Ricerca in corso..." : "📍 Trova Coordinate"}
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="latitude"
render={({ field }) => (
<FormItem>
<FormLabel>Latitudine</FormLabel>
<FormControl>
<Input
placeholder="41.9028"
{...field}
value={field.value || ""}
data-testid="input-latitude"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="longitude"
render={({ field }) => (
<FormItem>
<FormLabel>Longitudine</FormLabel>
<FormControl>
<Input
placeholder="12.4964"
{...field}
value={field.value || ""}
data-testid="input-longitude"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<p className="text-xs text-muted-foreground">
Le coordinate GPS permettono di visualizzare il sito sulla mappa in Planning Mobile
</p>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="customerId" name="customerId"
@ -521,6 +666,69 @@ export default function Sites() {
)} )}
/> />
<div className="border rounded-lg p-4 space-y-4 bg-muted/50">
<div className="flex items-center justify-between">
<p className="text-sm font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" />
Coordinate GPS (per mappa)
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleGeocodeEdit}
disabled={isGeocodingEdit || !editForm.watch("address")}
data-testid="button-geocode-edit"
>
{isGeocodingEdit ? "Ricerca in corso..." : "📍 Trova Coordinate"}
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="latitude"
render={({ field }) => (
<FormItem>
<FormLabel>Latitudine</FormLabel>
<FormControl>
<Input
placeholder="41.9028"
{...field}
value={field.value || ""}
data-testid="input-edit-latitude"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="longitude"
render={({ field }) => (
<FormItem>
<FormLabel>Longitudine</FormLabel>
<FormControl>
<Input
placeholder="12.4964"
{...field}
value={field.value || ""}
data-testid="input-edit-longitude"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<p className="text-xs text-muted-foreground">
Le coordinate GPS permettono di visualizzare il sito sulla mappa in Planning Mobile
</p>
</div>
<FormField <FormField
control={editForm.control} control={editForm.control}
name="customerId" name="customerId"

View File

@ -3332,9 +3332,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
nominatimUrl.searchParams.set("limit", "1"); nominatimUrl.searchParams.set("limit", "1");
nominatimUrl.searchParams.set("addressdetails", "1"); nominatimUrl.searchParams.set("addressdetails", "1");
// Nominatim Usage Policy richiede User-Agent con contatto email
// Ref: https://operations.osmfoundation.org/policies/nominatim/
const response = await fetch(nominatimUrl.toString(), { const response = await fetch(nominatimUrl.toString(), {
headers: { headers: {
"User-Agent": "VigilanzaTurni/1.0 (Security Shift Management System)", "User-Agent": "VigilanzaTurni/1.0 (Security Shift Management System; contact: support@vigilanzaturni.it)",
}, },
}); });