Compare commits

...

8 Commits

Author SHA1 Message Date
Marco Lanzara
66dc97855e 🚀 Release v1.0.40
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.40_20251023_104924.sql.gz
- Data: 2025-10-23 10:49:40
2025-10-23 10:49:40 +00:00
marco370
90f5061d95 Add feature to display upcoming planned shifts for guards
Update Guard and Shift models and related views to fetch and display upcoming shifts for guards.

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 10:48:47 +00:00
marco370
33b69f5ecc Add interactive map to mobile planning and fix backend issues
Integrate Leaflet map into Planning Mobile, displaying sites with GPS coordinates and automatic re-centering. Fix backend issues related to site and guard data, service type joins, and order by syntax.

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:48:23 +00:00
marco370
7431145ee3 Integrate map functionality for planning mobile view
Add Leaflet map integration to the planning mobile view, displaying sites with coordinates and filtering guards with driver licenses.

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:46:54 +00:00
marco370
0cfa154e61 Add mapping capabilities for patrol planning and guard assignments
Integrate Leaflet and React Leaflet libraries to enable map display and functionality for patrol planning and guard assignments.

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/AXEqh9q
2025-10-23 10:40:52 +00:00
marco370
bf5cfdcd50 Show only guards with driver's licenses and update site filtering
Modify the query to include guards with driver's licenses and use a left join for service types.

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/AXEqh9q
2025-10-23 10:38:40 +00:00
marco370
392079d2a1 Update site management to use dynamic service types
Integrate `serviceTypeId` FK in the `sites` table to link to `service_types` table, replacing deprecated `shiftType` field. Modify site creation and editing forms to dynamically load and select service types, and update card display to show service type labels from the database.

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/AXEqh9q
2025-10-23 10:28:08 +00:00
marco370
a48577c9b8 Update site management to use service types instead of shift types
Introduces the ability to select service types when creating or editing sites, replacing the previous shift type field. It fetches available active service types from the API and displays them in a dropdown.

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/AXEqh9q
2025-10-23 10:25:43 +00:00
10 changed files with 217 additions and 60 deletions

View File

@ -18,6 +18,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head>
<body>
<div id="root"></div>

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@ -9,6 +9,16 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Calendar, MapPin, User, Car, Clock } from "lucide-react";
import { format, parseISO, isValid } from "date-fns";
import { it } from "date-fns/locale";
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
// Fix Leaflet default icon issue with Webpack
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
});
type Location = "roccapiemonte" | "milano" | "roma";
@ -16,12 +26,11 @@ type MobileSite = {
id: string;
name: string;
address: string;
city: string;
serviceTypeId: string;
serviceTypeName: string;
serviceTypeId: string | null;
serviceTypeName: string | null;
location: Location;
latitude: number | null;
longitude: number | null;
latitude: string | null;
longitude: string | null;
};
type AvailableGuard = {
@ -77,6 +86,23 @@ export default function PlanningMobile() {
roma: "bg-purple-500",
};
// Coordinate di default per centrare la mappa sulla location selezionata
const locationCenters: Record<Location, [number, number]> = {
roccapiemonte: [40.8167, 14.6167], // Roccapiemonte, Salerno
milano: [45.4642, 9.1900], // Milano
roma: [41.9028, 12.4964], // Roma
};
// Filtra siti con coordinate valide per la mappa
const sitesWithCoordinates = useMemo(() => {
return mobileSites?.filter((site) =>
site.latitude !== null &&
site.longitude !== null &&
!isNaN(parseFloat(site.latitude)) &&
!isNaN(parseFloat(site.longitude))
) || [];
}, [mobileSites]);
return (
<div className="h-full flex flex-col p-6 space-y-6">
{/* Header */}
@ -154,15 +180,50 @@ export default function PlanningMobile() {
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-lg">
<div className="text-center space-y-2">
<CardContent className="flex-1 p-0 overflow-hidden">
{sitesWithCoordinates.length > 0 ? (
<MapContainer
key={selectedLocation}
center={locationCenters[selectedLocation]}
zoom={12}
className="h-full w-full min-h-[400px]"
data-testid="map-container"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{sitesWithCoordinates.map((site) => (
<Marker
key={site.id}
position={[parseFloat(site.latitude!), parseFloat(site.longitude!)]}
>
<Popup>
<div className="space-y-1">
<h4 className="font-semibold text-sm">{site.name}</h4>
<p className="text-xs text-muted-foreground">{site.address}</p>
{site.serviceTypeName && (
<Badge variant="outline" className="text-xs">
{site.serviceTypeName}
</Badge>
)}
</div>
</Popup>
</Marker>
))}
</MapContainer>
) : (
<div className="h-full flex items-center justify-center bg-muted/20">
<div className="text-center space-y-2 p-6">
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Integrazione mappa in sviluppo
Nessun sito con coordinate GPS disponibile
<br />
(Leaflet/Google Maps)
<span className="text-xs">Aggiungi latitudine e longitudine ai siti per visualizzarli sulla mappa</span>
</p>
</div>
</div>
)}
</CardContent>
</Card>
@ -192,18 +253,20 @@ export default function PlanningMobile() {
<h4 className="font-semibold">{site.name}</h4>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<MapPin className="h-3 w-3" />
{site.address}, {site.city}
{site.address}
</p>
</div>
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
{locationLabels[site.location]}
</Badge>
</div>
{site.serviceTypeName && (
<div className="flex items-center gap-2 text-sm">
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
{site.serviceTypeName}
</Badge>
</div>
)}
<div className="flex gap-2 pt-2">
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
Assegna Guardia

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Site, InsertSite, Customer } from "@shared/schema";
import { Site, InsertSite, Customer, ServiceType } from "@shared/schema";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
@ -18,13 +18,6 @@ import { StatusBadge } from "@/components/status-badge";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
const shiftTypeLabels: Record<string, string> = {
fixed_post: "Presidio Fisso",
patrol: "Pattugliamento",
night_inspection: "Ispettorato Notturno",
quick_response: "Pronto Intervento",
};
const locationLabels: Record<string, string> = {
roccapiemonte: "Roccapiemonte",
milano: "Milano",
@ -44,6 +37,10 @@ export default function Sites() {
queryKey: ["/api/customers"],
});
const { data: serviceTypes } = useQuery<ServiceType[]>({
queryKey: ["/api/service-types"],
});
const form = useForm<InsertSite>({
resolver: zodResolver(insertSiteSchema),
defaultValues: {
@ -51,7 +48,7 @@ export default function Sites() {
address: "",
customerId: undefined,
location: "roccapiemonte",
shiftType: "fixed_post",
serviceTypeId: undefined,
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
@ -71,7 +68,7 @@ export default function Sites() {
address: "",
customerId: undefined,
location: "roccapiemonte",
shiftType: "fixed_post",
serviceTypeId: undefined,
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
@ -145,7 +142,7 @@ export default function Sites() {
address: site.address,
customerId: site.customerId ?? undefined,
location: site.location,
shiftType: site.shiftType,
serviceTypeId: site.serviceTypeId ?? undefined,
minGuards: site.minGuards,
requiresArmed: site.requiresArmed,
requiresDriverLicense: site.requiresDriverLicense,
@ -345,21 +342,22 @@ export default function Sites() {
<FormField
control={form.control}
name="shiftType"
name="serviceTypeId"
render={({ field }) => (
<FormItem>
<FormLabel>Tipologia Servizio</FormLabel>
<FormLabel>Tipologia Servizio (opzionale)</FormLabel>
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
<FormControl>
<SelectTrigger data-testid="select-shift-type">
<SelectTrigger data-testid="select-service-type">
<SelectValue placeholder="Seleziona tipo servizio" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
{serviceTypes?.filter(st => st.isActive).map((serviceType) => (
<SelectItem key={serviceType.id} value={serviceType.id}>
{serviceType.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
@ -631,21 +629,22 @@ export default function Sites() {
<FormField
control={editForm.control}
name="shiftType"
name="serviceTypeId"
render={({ field }) => (
<FormItem>
<FormLabel>Tipologia Servizio</FormLabel>
<FormLabel>Tipologia Servizio (opzionale)</FormLabel>
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
<FormControl>
<SelectTrigger data-testid="select-edit-shift-type">
<SelectTrigger data-testid="select-edit-service-type">
<SelectValue placeholder="Seleziona tipo servizio" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
{serviceTypes?.filter(st => st.isActive).map((serviceType) => (
<SelectItem key={serviceType.id} value={serviceType.id}>
{serviceType.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
@ -824,9 +823,14 @@ export default function Sites() {
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
<Badge variant="outline">
{shiftTypeLabels[site.shiftType]}
{site.serviceTypeId && serviceTypes && (() => {
const serviceType = serviceTypes.find(st => st.id === site.serviceTypeId);
return serviceType ? (
<Badge variant="outline" data-testid={`badge-service-type-${site.id}`}>
{serviceType.label}
</Badge>
) : null;
})()}
{(() => {
const status = getContractStatus(site);
const statusInfo = contractStatusLabels[status];

49
package-lock.json generated
View File

@ -41,6 +41,7 @@
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/leaflet": "^1.9.21",
"@types/memoizee": "^0.4.12",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
@ -56,6 +57,7 @@
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
"input-otp": "^1.4.2",
"leaflet": "^1.9.4",
"lucide-react": "^0.453.0",
"memoizee": "^0.4.17",
"memorystore": "^1.6.7",
@ -69,6 +71,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-icons": "^5.4.0",
"react-leaflet": "^4.2.1",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"tailwind-merge": "^2.6.0",
@ -2790,6 +2793,17 @@
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
},
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@replit/vite-plugin-cartographer": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@replit/vite-plugin-cartographer/-/vite-plugin-cartographer-0.3.1.tgz",
@ -3571,6 +3585,12 @@
"@types/express": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -3578,6 +3598,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/memoizee": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz",
@ -5549,6 +5578,12 @@
"node": ">=6"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
@ -6820,6 +6855,20 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-leaflet": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^2.1.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@ -43,6 +43,7 @@
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/leaflet": "^1.9.21",
"@types/memoizee": "^0.4.12",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
@ -58,6 +59,7 @@
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
"input-otp": "^1.4.2",
"leaflet": "^1.9.4",
"lucide-react": "^0.453.0",
"memoizee": "^0.4.17",
"memorystore": "^1.6.7",
@ -71,6 +73,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-icons": "^5.4.0",
"react-leaflet": "^4.2.1",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"tailwind-merge": "^2.6.0",

View File

@ -19,6 +19,7 @@ VigilanzaTurni is a professional 24/7 shift management system for security compa
- **Autenticazione**: Replit Auth (OIDC)
- **State Management**: TanStack Query v5
- **Routing**: Wouter
- **Maps**: Leaflet + react-leaflet + OpenStreetMap tiles
### Design System
- **Font Principale**: Inter (sans-serif)
@ -28,18 +29,18 @@ VigilanzaTurni is a professional 24/7 shift management system for security compa
- **Componenti**: Shadcn UI with an operational design.
### Database Schema
The database includes tables for `users`, `guards`, `certifications`, `sites`, `shifts`, `shift_assignments`, `notifications`, `customers`, and various tables for advanced scheduling and constraints (`guard_constraints`, `site_preferences`, `contract_parameters`, `training_courses`, `holidays`, `holiday_assignments`, `absences`, `absence_affected_shifts`). Service types include specialized parameters like `fixedPostHours`, `patrolPassages`, `inspectionFrequency`, and `responseTimeMinutes`. Sites support multi-location (`location` field) and contract management (`contractReference`, `contractStartDate`, `contractEndDate`).
The database includes tables for `users`, `guards`, `certifications`, `sites`, `shifts`, `shift_assignments`, `notifications`, `customers`, `service_types`, and various tables for advanced scheduling and constraints (`guard_constraints`, `site_preferences`, `contract_parameters`, `training_courses`, `holidays`, `holiday_assignments`, `absences`, `absence_affected_shifts`). Service types include specialized parameters like `fixedPostHours`, `patrolPassages`, `inspectionFrequency`, and `responseTimeMinutes`. Sites support multi-location (`location` field), contract management (`contractReference`, `contractStartDate`, `contractEndDate`), and service type association (`serviceTypeId` FK to `service_types.id`).
### Core Features
- **Multi-Sede Operational Planning**: Location-first approach for shift planning, filtering sites, guards, and vehicles by selected branch.
- **Service Type Classification**: Service types are classified as "fisso" (fixed posts) or "mobile" (patrols, inspections) to route sites to appropriate planning modules (Planning Fissi, Planning Mobile).
- **Planning Fissi**: Weekly planning grid showing all sites with active contracts, allowing direct shift creation for multiple days with guard availability checks.
- **Planning Mobile**: Dedicated guard-centric interface for mobile services, displaying guard availability and hours for mobile-classified sites. Includes a map placeholder for future integration.
- **Planning Mobile**: Dedicated guard-centric interface for mobile services, displaying guard availability and hours for mobile-classified sites. Includes interactive Leaflet map showing sites with GPS coordinates and automatic re-centering based on selected location.
- **Customer Management**: Full CRUD operations for customers with comprehensive details.
- **Customer-Centric Reports**: New reports aggregating data by customer, replacing site-based billing, with specific counters for fixed posts (hours), patrols (passages), inspections, and interventions. CSV export is supported.
- **Dashboard Operativa**: Live KPIs and real-time shift status.
- **Gestione Guardie**: Complete profiles with skill matrix, certification management, and unique badge numbers.
- **Gestione Siti/Commesse**: Service types with specialized parameters and minimum requirements. Sites include service schedule, contract management, and location assignment.
- **Gestione Siti/Commesse**: Sites are associated with service types from the `service_types` table via `serviceTypeId` (FK). Service types are managed in the "Tipologie Servizi" page and include specialized parameters. Sites include service schedule, contract management, location assignment, and customer assignment (`customerId` FK to `customers.id`).
- **Pianificazione Turni**: 24/7 calendar, manual guard assignment, basic constraints, and shift statuses.
- **Advanced Planning**: Management of guard constraints, site preferences, contract parameters, training courses, holidays, and absences.
@ -52,6 +53,32 @@ The database includes tables for `users`, `guards`, `certifications`, `sites`, `
### Critical Date/Timezone Handling
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)
### Planning Mobile - Leaflet Map Integration (October 23, 2025)
- **Issue**: Planning Mobile page had errors in backend endpoints and lacked interactive map functionality
- **Solution**:
- **Backend Fixes**:
- Removed non-existent fields (`city` from sites, `isActive` from guards) from queries
- Changed `innerJoin` to `leftJoin` for service types to handle sites without serviceTypeId
- Fixed `orderBy` syntax (single field instead of multiple)
- Added `hasDriverLicense` filter for guards (mobile services require driving)
- **Map Integration**:
- Implemented Leaflet + react-leaflet with OpenStreetMap tiles (100% free, no API key)
- Interactive markers for sites with GPS coordinates and classification="mobile"
- Popup details showing site name, address, and service type
- Automatic re-centering via `key={selectedLocation}` forcing MapContainer remount
- Graceful fallback for sites without coordinates
- **Impact**: Planning Mobile now fully functional with interactive map for patrol/inspection route planning
### Sites Form Fix - ServiceTypeId Integration (October 2025)
- **Issue**: Sites form used hardcoded `shiftType` enum values instead of dynamic service types from the database
- **Solution**:
- Changed Sites form to use `serviceTypeId` (FK to `service_types.id`) instead of deprecated `shiftType` field
- Added dynamic service type dropdown loading from `/api/service-types` endpoint
- Updated both create and edit forms to properly handle service type selection
- Card display now shows service type label from database instead of hardcoded labels
- **Impact**: Sites now correctly reference service types configured in "Tipologie Servizi" page, ensuring consistency across the system
## External Dependencies
- **Replit Auth**: For OpenID Connect (OIDC) based authentication.
- **Neon**: Managed PostgreSQL database service.
@ -61,3 +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).

View File

@ -3191,7 +3191,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
id: sites.id,
name: sites.name,
address: sites.address,
city: sites.city,
serviceTypeId: sites.serviceTypeId,
serviceTypeName: serviceTypes.label,
location: sites.location,
@ -3199,7 +3198,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
longitude: sites.longitude,
})
.from(sites)
.innerJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
.leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
.where(
and(
eq(sites.location, location as "roccapiemonte" | "milano" | "roma"),
@ -3235,17 +3234,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(400).json({ message: "Invalid date format (use YYYY-MM-DD)" });
}
// Ottieni tutte le guardie per location
// Ottieni tutte le guardie per location CHE HANNO LA PATENTE
const allGuards = await db
.select()
.from(guards)
.where(
and(
eq(guards.location, location as "roccapiemonte" | "milano" | "roma"),
eq(guards.isActive, true)
eq(guards.hasDriverLicense, true)
)
)
.orderBy(guards.lastName, guards.firstName);
.orderBy(guards.lastName);
// Calcola settimana corrente per calcolare ore settimanali
const [year, month, day] = date.split("-").map(Number);

View File

@ -1,7 +1,13 @@
{
"version": "1.0.39",
"lastUpdate": "2025-10-23T10:18:17.289Z",
"version": "1.0.40",
"lastUpdate": "2025-10-23T10:49:40.822Z",
"changelog": [
{
"version": "1.0.40",
"date": "2025-10-23",
"type": "patch",
"description": "Deployment automatico v1.0.40"
},
{
"version": "1.0.39",
"date": "2025-10-23",