Compare commits

..

No commits in common. "3b2ac3d0cd636f887a73a1dddae5a133fb211170" and "66dc97855eb960f868249e90c8a1b8399181903a" have entirely different histories.

6 changed files with 3 additions and 298 deletions

View File

@ -28,8 +28,6 @@ export default function Sites() {
const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingSite, setEditingSite] = useState<Site | null>(null);
const [isGeocoding, setIsGeocoding] = useState(false);
const [isGeocodingEdit, setIsGeocodingEdit] = useState(false);
const { data: sites, isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
@ -59,8 +57,6 @@ export default function Sites() {
contractEndDate: undefined,
serviceStartTime: "",
serviceEndTime: "",
latitude: undefined,
longitude: undefined,
isActive: true,
},
});
@ -81,8 +77,6 @@ export default function Sites() {
contractEndDate: undefined,
serviceStartTime: "",
serviceEndTime: "",
latitude: undefined,
longitude: undefined,
isActive: true,
},
});
@ -131,80 +125,6 @@ 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) => {
createMutation.mutate(data);
};
@ -231,8 +151,6 @@ export default function Sites() {
contractEndDate: site.contractEndDate || undefined,
serviceStartTime: site.serviceStartTime || "",
serviceEndTime: site.serviceEndTime || "",
latitude: site.latitude || undefined,
longitude: site.longitude || undefined,
isActive: site.isActive,
});
};
@ -316,69 +234,6 @@ 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
control={form.control}
name="customerId"
@ -666,69 +521,6 @@ 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
control={editForm.control}
name="customerId"

View File

@ -54,22 +54,6 @@ The database includes tables for `users`, `guards`, `certifications`, `sites`, `
To prevent timezone-related bugs, especially when assigning shifts, dates should always be constructed from components (`new Date(year, month-1, day)`) and never parsed from ISO strings directly using `parseISO()` or `new Date(string ISO)`. Date validation should use regex instead of `parseISO()`.
## Recent Changes (October 2025)
### Automatic Geocoding Integration (October 23, 2025)
- **Issue**: Users had to manually find and enter GPS coordinates for sites, which was time-consuming and error-prone
- **Solution**:
- **Backend (`server/routes.ts`)**:
- Created POST `/api/geocode` endpoint integrating Nominatim API (OpenStreetMap)
- Implemented in-memory rate limiter enforcing 1 request/second to comply with Nominatim usage policy
- Added compliant User-Agent header: "VigilanzaTurni/1.0 (Security Shift Management System; contact: support@vigilanzaturni.it)"
- Returns latitude, longitude, displayName, and full address object
- **Frontend (`client/src/pages/sites.tsx`)**:
- Added "📍 Trova Coordinate" button in both create and edit site dialogs
- Button auto-populates latitude/longitude fields from address
- Disabled state when address missing or geocoding in progress
- Toast notifications for success (with found address) and error handling
- Dedicated section with bg-muted/50 highlighting for GPS coordinates
- **Impact**: Users can now automatically geocode site addresses with a single click, ensuring accurate GPS positioning for Planning Mobile map without manual coordinate lookup
### Planning Mobile - Leaflet Map Integration (October 23, 2025)
- **Issue**: Planning Mobile page had errors in backend endpoints and lacked interactive map functionality
- **Solution**:
@ -104,5 +88,4 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
- **TanStack Query**: For data fetching and state management.
- **Wouter**: For client-side routing.
- **date-fns**: For date manipulation and formatting.
- **Leaflet**: Interactive map library with react-leaflet bindings and OpenStreetMap tiles (free).
- **Nominatim**: OpenStreetMap geocoding API for automatic address-to-coordinates conversion (free, rate limited to 1 req/sec).
- **Leaflet**: Interactive map library with react-leaflet bindings and OpenStreetMap tiles (free).

View File

@ -3303,70 +3303,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// ============= GEOCODING API (Nominatim/OSM) =============
// Rate limiter semplice per rispettare 1 req/sec di Nominatim
let lastGeocodingRequest = 0;
app.post("/api/geocode", isAuthenticated, async (req: any, res) => {
try {
const { address } = req.body;
if (!address || typeof address !== 'string') {
return res.status(400).json({ message: "Address parameter required" });
}
// Rispetta rate limit di 1 req/sec
const now = Date.now();
const timeSinceLastRequest = now - lastGeocodingRequest;
if (timeSinceLastRequest < 1000) {
const waitTime = 1000 - timeSinceLastRequest;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
lastGeocodingRequest = Date.now();
// Chiama Nominatim API
const nominatimUrl = new URL("https://nominatim.openstreetmap.org/search");
nominatimUrl.searchParams.set("q", address);
nominatimUrl.searchParams.set("format", "json");
nominatimUrl.searchParams.set("limit", "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(), {
headers: {
"User-Agent": "VigilanzaTurni/1.0 (Security Shift Management System; contact: support@vigilanzaturni.it)",
},
});
if (!response.ok) {
throw new Error(`Nominatim API error: ${response.status}`);
}
const data = await response.json();
if (!data || data.length === 0) {
return res.status(404).json({
message: "Indirizzo non trovato. Prova a essere più specifico (es. Via, Città, Italia)"
});
}
const result = data[0];
res.json({
latitude: result.lat,
longitude: result.lon,
displayName: result.display_name,
address: result.address,
});
} catch (error) {
console.error("Error geocoding address:", error);
res.status(500).json({ message: "Errore durante la geocodifica dell'indirizzo" });
}
});
const httpServer = createServer(app);
return httpServer;
}

View File

@ -1,13 +1,7 @@
{
"version": "1.0.41",
"lastUpdate": "2025-10-23T13:41:37.302Z",
"version": "1.0.40",
"lastUpdate": "2025-10-23T10:49:40.822Z",
"changelog": [
{
"version": "1.0.41",
"date": "2025-10-23",
"type": "patch",
"description": "Deployment automatico v1.0.41"
},
{
"version": "1.0.40",
"date": "2025-10-23",