Compare commits
No commits in common. "3b2ac3d0cd636f887a73a1dddae5a133fb211170" and "66dc97855eb960f868249e90c8a1b8399181903a" have entirely different histories.
3b2ac3d0cd
...
66dc97855e
@ -28,8 +28,6 @@ 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"],
|
||||||
@ -59,8 +57,6 @@ export default function Sites() {
|
|||||||
contractEndDate: undefined,
|
contractEndDate: undefined,
|
||||||
serviceStartTime: "",
|
serviceStartTime: "",
|
||||||
serviceEndTime: "",
|
serviceEndTime: "",
|
||||||
latitude: undefined,
|
|
||||||
longitude: undefined,
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -81,8 +77,6 @@ export default function Sites() {
|
|||||||
contractEndDate: undefined,
|
contractEndDate: undefined,
|
||||||
serviceStartTime: "",
|
serviceStartTime: "",
|
||||||
serviceEndTime: "",
|
serviceEndTime: "",
|
||||||
latitude: undefined,
|
|
||||||
longitude: undefined,
|
|
||||||
isActive: true,
|
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) => {
|
const onSubmit = (data: InsertSite) => {
|
||||||
createMutation.mutate(data);
|
createMutation.mutate(data);
|
||||||
};
|
};
|
||||||
@ -231,8 +151,6 @@ 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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="customerId"
|
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
|
<FormField
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
name="customerId"
|
name="customerId"
|
||||||
|
|||||||
BIN
database-backups/vigilanzaturni_v1.0.31_20251022_081911.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.31_20251022_081911.sql.gz
Normal file
Binary file not shown.
Binary file not shown.
19
replit.md
19
replit.md
@ -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()`.
|
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**:
|
||||||
@ -104,5 +88,4 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
|
|||||||
- **TanStack Query**: For data fetching and state management.
|
- **TanStack Query**: For data fetching and state management.
|
||||||
- **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).
|
|
||||||
@ -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);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.41",
|
"version": "1.0.40",
|
||||||
"lastUpdate": "2025-10-23T13:41:37.302Z",
|
"lastUpdate": "2025-10-23T10:49:40.822Z",
|
||||||
"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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user