Compare commits

...

5 Commits

Author SHA1 Message Date
Marco Lanzara
3b2ac3d0cd 🚀 Release v1.0.41
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.41_20251023_134119.sql.gz
- Data: 2025-10-23 13:41:37
2025-10-23 13:41:37 +00:00
marco370
c66642e0a1 Add functionality for tracking guard movements using geofencing
Implement geofencing feature to monitor guard location and ensure adherence to assigned patrol routes.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/TFybNy5
2025-10-23 11:08:32 +00:00
marco370
db67aa9f61 Add automatic geocoding for site addresses to improve GPS accuracy
Integrate Nominatim API via a new backend endpoint and frontend button to automatically convert site addresses into GPS coordinates, enhancing accuracy for the mobile planning map.

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
2025-10-23 11:08:10 +00:00
marco370
0c702f4dbf 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
2025-10-23 11:06:56 +00:00
marco370
db860125fc Add address lookup using OpenStreetMap data
Integrate Nominatim API endpoint to geocode addresses with rate limiting and user-agent configuration.

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
2025-10-23 10:59:32 +00:00
6 changed files with 298 additions and 3 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

@ -54,6 +54,22 @@ 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()`. 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) ## 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) ### Planning Mobile - Leaflet Map Integration (October 23, 2025)
- **Issue**: Planning Mobile page had errors in backend endpoints and lacked interactive map functionality - **Issue**: Planning Mobile page had errors in backend endpoints and lacked interactive map functionality
- **Solution**: - **Solution**:
@ -89,3 +105,4 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
- **Wouter**: For client-side routing. - **Wouter**: For client-side routing.
- **date-fns**: For date manipulation and formatting. - **date-fns**: For date manipulation and formatting.
- **Leaflet**: Interactive map library with react-leaflet bindings and OpenStreetMap tiles (free). - **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).

View File

@ -3303,6 +3303,70 @@ 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); const httpServer = createServer(app);
return httpServer; return httpServer;
} }

View File

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