Compare commits
5 Commits
66dc97855e
...
3b2ac3d0cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b2ac3d0cd | ||
|
|
c66642e0a1 | ||
|
|
db67aa9f61 | ||
|
|
0c702f4dbf | ||
|
|
db860125fc |
@ -28,6 +28,8 @@ 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"],
|
||||
@ -57,6 +59,8 @@ export default function Sites() {
|
||||
contractEndDate: undefined,
|
||||
serviceStartTime: "",
|
||||
serviceEndTime: "",
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
@ -77,6 +81,8 @@ export default function Sites() {
|
||||
contractEndDate: undefined,
|
||||
serviceStartTime: "",
|
||||
serviceEndTime: "",
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
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) => {
|
||||
createMutation.mutate(data);
|
||||
};
|
||||
@ -151,6 +231,8 @@ 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,
|
||||
});
|
||||
};
|
||||
@ -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
|
||||
control={form.control}
|
||||
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
|
||||
control={editForm.control}
|
||||
name="customerId"
|
||||
|
||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.41_20251023_134119.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.41_20251023_134119.sql.gz
Normal file
Binary file not shown.
19
replit.md
19
replit.md
@ -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()`.
|
||||
|
||||
## 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**:
|
||||
@ -88,4 +104,5 @@ 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).
|
||||
- **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).
|
||||
@ -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);
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
||||
{
|
||||
"version": "1.0.40",
|
||||
"lastUpdate": "2025-10-23T10:49:40.822Z",
|
||||
"version": "1.0.41",
|
||||
"lastUpdate": "2025-10-23T13:41:37.302Z",
|
||||
"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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user