Compare commits
8 Commits
9ee37d8ea1
...
66dc97855e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66dc97855e | ||
|
|
90f5061d95 | ||
|
|
33b69f5ecc | ||
|
|
7431145ee3 | ||
|
|
0cfa154e61 | ||
|
|
bf5cfdcd50 | ||
|
|
392079d2a1 | ||
|
|
a48577c9b8 |
@ -18,6 +18,11 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Calendar, MapPin, User, Car, Clock } from "lucide-react";
|
||||||
import { format, parseISO, isValid } from "date-fns";
|
import { format, parseISO, isValid } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
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";
|
type Location = "roccapiemonte" | "milano" | "roma";
|
||||||
|
|
||||||
@ -16,12 +26,11 @@ type MobileSite = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
city: string;
|
serviceTypeId: string | null;
|
||||||
serviceTypeId: string;
|
serviceTypeName: string | null;
|
||||||
serviceTypeName: string;
|
|
||||||
location: Location;
|
location: Location;
|
||||||
latitude: number | null;
|
latitude: string | null;
|
||||||
longitude: number | null;
|
longitude: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AvailableGuard = {
|
type AvailableGuard = {
|
||||||
@ -77,6 +86,23 @@ export default function PlanningMobile() {
|
|||||||
roma: "bg-purple-500",
|
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 (
|
return (
|
||||||
<div className="h-full flex flex-col p-6 space-y-6">
|
<div className="h-full flex flex-col p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -154,15 +180,50 @@ export default function PlanningMobile() {
|
|||||||
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
|
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-lg">
|
<CardContent className="flex-1 p-0 overflow-hidden">
|
||||||
<div className="text-center space-y-2">
|
{sitesWithCoordinates.length > 0 ? (
|
||||||
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
|
<MapContainer
|
||||||
<p className="text-sm text-muted-foreground">
|
key={selectedLocation}
|
||||||
Integrazione mappa in sviluppo
|
center={locationCenters[selectedLocation]}
|
||||||
<br />
|
zoom={12}
|
||||||
(Leaflet/Google Maps)
|
className="h-full w-full min-h-[400px]"
|
||||||
</p>
|
data-testid="map-container"
|
||||||
</div>
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <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">
|
||||||
|
Nessun sito con coordinate GPS disponibile
|
||||||
|
<br />
|
||||||
|
<span className="text-xs">Aggiungi latitudine e longitudine ai siti per visualizzarli sulla mappa</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -192,18 +253,20 @@ export default function PlanningMobile() {
|
|||||||
<h4 className="font-semibold">{site.name}</h4>
|
<h4 className="font-semibold">{site.name}</h4>
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
<MapPin className="h-3 w-3" />
|
<MapPin className="h-3 w-3" />
|
||||||
{site.address}, {site.city}
|
{site.address}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
|
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
|
||||||
{locationLabels[site.location]}
|
{locationLabels[site.location]}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
{site.serviceTypeName && (
|
||||||
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
|
<div className="flex items-center gap-2 text-sm">
|
||||||
{site.serviceTypeName}
|
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
|
||||||
</Badge>
|
{site.serviceTypeName}
|
||||||
</div>
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
|
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
|
||||||
Assegna Guardia
|
Assegna Guardia
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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> = {
|
const locationLabels: Record<string, string> = {
|
||||||
roccapiemonte: "Roccapiemonte",
|
roccapiemonte: "Roccapiemonte",
|
||||||
milano: "Milano",
|
milano: "Milano",
|
||||||
@ -44,6 +37,10 @@ export default function Sites() {
|
|||||||
queryKey: ["/api/customers"],
|
queryKey: ["/api/customers"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: serviceTypes } = useQuery<ServiceType[]>({
|
||||||
|
queryKey: ["/api/service-types"],
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<InsertSite>({
|
const form = useForm<InsertSite>({
|
||||||
resolver: zodResolver(insertSiteSchema),
|
resolver: zodResolver(insertSiteSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -51,7 +48,7 @@ export default function Sites() {
|
|||||||
address: "",
|
address: "",
|
||||||
customerId: undefined,
|
customerId: undefined,
|
||||||
location: "roccapiemonte",
|
location: "roccapiemonte",
|
||||||
shiftType: "fixed_post",
|
serviceTypeId: undefined,
|
||||||
minGuards: 1,
|
minGuards: 1,
|
||||||
requiresArmed: false,
|
requiresArmed: false,
|
||||||
requiresDriverLicense: false,
|
requiresDriverLicense: false,
|
||||||
@ -71,7 +68,7 @@ export default function Sites() {
|
|||||||
address: "",
|
address: "",
|
||||||
customerId: undefined,
|
customerId: undefined,
|
||||||
location: "roccapiemonte",
|
location: "roccapiemonte",
|
||||||
shiftType: "fixed_post",
|
serviceTypeId: undefined,
|
||||||
minGuards: 1,
|
minGuards: 1,
|
||||||
requiresArmed: false,
|
requiresArmed: false,
|
||||||
requiresDriverLicense: false,
|
requiresDriverLicense: false,
|
||||||
@ -145,7 +142,7 @@ export default function Sites() {
|
|||||||
address: site.address,
|
address: site.address,
|
||||||
customerId: site.customerId ?? undefined,
|
customerId: site.customerId ?? undefined,
|
||||||
location: site.location,
|
location: site.location,
|
||||||
shiftType: site.shiftType,
|
serviceTypeId: site.serviceTypeId ?? undefined,
|
||||||
minGuards: site.minGuards,
|
minGuards: site.minGuards,
|
||||||
requiresArmed: site.requiresArmed,
|
requiresArmed: site.requiresArmed,
|
||||||
requiresDriverLicense: site.requiresDriverLicense,
|
requiresDriverLicense: site.requiresDriverLicense,
|
||||||
@ -345,21 +342,22 @@ export default function Sites() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="shiftType"
|
name="serviceTypeId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Tipologia Servizio</FormLabel>
|
<FormLabel>Tipologia Servizio (opzionale)</FormLabel>
|
||||||
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
|
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger data-testid="select-shift-type">
|
<SelectTrigger data-testid="select-service-type">
|
||||||
<SelectValue placeholder="Seleziona tipo servizio" />
|
<SelectValue placeholder="Seleziona tipo servizio" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
|
{serviceTypes?.filter(st => st.isActive).map((serviceType) => (
|
||||||
<SelectItem value="patrol">Pattugliamento</SelectItem>
|
<SelectItem key={serviceType.id} value={serviceType.id}>
|
||||||
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
|
{serviceType.label}
|
||||||
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -631,21 +629,22 @@ export default function Sites() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
name="shiftType"
|
name="serviceTypeId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Tipologia Servizio</FormLabel>
|
<FormLabel>Tipologia Servizio (opzionale)</FormLabel>
|
||||||
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
|
<Select onValueChange={field.onChange} value={field.value ?? undefined}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger data-testid="select-edit-shift-type">
|
<SelectTrigger data-testid="select-edit-service-type">
|
||||||
<SelectValue placeholder="Seleziona tipo servizio" />
|
<SelectValue placeholder="Seleziona tipo servizio" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
|
{serviceTypes?.filter(st => st.isActive).map((serviceType) => (
|
||||||
<SelectItem value="patrol">Pattugliamento</SelectItem>
|
<SelectItem key={serviceType.id} value={serviceType.id}>
|
||||||
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
|
{serviceType.label}
|
||||||
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -824,9 +823,14 @@ export default function Sites() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Badge variant="outline">
|
{site.serviceTypeId && serviceTypes && (() => {
|
||||||
{shiftTypeLabels[site.shiftType]}
|
const serviceType = serviceTypes.find(st => st.id === site.serviceTypeId);
|
||||||
</Badge>
|
return serviceType ? (
|
||||||
|
<Badge variant="outline" data-testid={`badge-service-type-${site.id}`}>
|
||||||
|
{serviceType.label}
|
||||||
|
</Badge>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
{(() => {
|
{(() => {
|
||||||
const status = getContractStatus(site);
|
const status = getContractStatus(site);
|
||||||
const statusInfo = contractStatusLabels[status];
|
const statusInfo = contractStatusLabels[status];
|
||||||
|
|||||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.40_20251023_104924.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.40_20251023_104924.sql.gz
Normal file
Binary file not shown.
49
package-lock.json
generated
49
package-lock.json
generated
@ -41,6 +41,7 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
@ -69,6 +71,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"react-leaflet": "^4.2.1",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
@ -2790,6 +2793,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
|
"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": {
|
"node_modules/@replit/vite-plugin-cartographer": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@replit/vite-plugin-cartographer/-/vite-plugin-cartographer-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@replit/vite-plugin-cartographer/-/vite-plugin-cartographer-0.3.1.tgz",
|
||||||
@ -3571,6 +3585,12 @@
|
|||||||
"@types/express": "*"
|
"@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": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||||
@ -3578,6 +3598,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/memoizee": {
|
||||||
"version": "0.4.12",
|
"version": "0.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz",
|
||||||
@ -5549,6 +5578,12 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.29.2",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
@ -6820,6 +6855,20 @@
|
|||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
|||||||
@ -43,6 +43,7 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
@ -71,6 +73,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"react-leaflet": "^4.2.1",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
|||||||
36
replit.md
36
replit.md
@ -19,6 +19,7 @@ VigilanzaTurni is a professional 24/7 shift management system for security compa
|
|||||||
- **Autenticazione**: Replit Auth (OIDC)
|
- **Autenticazione**: Replit Auth (OIDC)
|
||||||
- **State Management**: TanStack Query v5
|
- **State Management**: TanStack Query v5
|
||||||
- **Routing**: Wouter
|
- **Routing**: Wouter
|
||||||
|
- **Maps**: Leaflet + react-leaflet + OpenStreetMap tiles
|
||||||
|
|
||||||
### Design System
|
### Design System
|
||||||
- **Font Principale**: Inter (sans-serif)
|
- **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.
|
- **Componenti**: Shadcn UI with an operational design.
|
||||||
|
|
||||||
### Database Schema
|
### 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
|
### Core Features
|
||||||
- **Multi-Sede Operational Planning**: Location-first approach for shift planning, filtering sites, guards, and vehicles by selected branch.
|
- **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).
|
- **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 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 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.
|
- **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.
|
- **Dashboard Operativa**: Live KPIs and real-time shift status.
|
||||||
- **Gestione Guardie**: Complete profiles with skill matrix, certification management, and unique badge numbers.
|
- **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.
|
- **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.
|
- **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
|
### 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()`.
|
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
|
## External Dependencies
|
||||||
- **Replit Auth**: For OpenID Connect (OIDC) based authentication.
|
- **Replit Auth**: For OpenID Connect (OIDC) based authentication.
|
||||||
- **Neon**: Managed PostgreSQL database service.
|
- **Neon**: Managed PostgreSQL database service.
|
||||||
@ -60,4 +87,5 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
|
|||||||
- **Zod**: For schema validation.
|
- **Zod**: For schema validation.
|
||||||
- **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).
|
||||||
@ -3191,7 +3191,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
id: sites.id,
|
id: sites.id,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
address: sites.address,
|
address: sites.address,
|
||||||
city: sites.city,
|
|
||||||
serviceTypeId: sites.serviceTypeId,
|
serviceTypeId: sites.serviceTypeId,
|
||||||
serviceTypeName: serviceTypes.label,
|
serviceTypeName: serviceTypes.label,
|
||||||
location: sites.location,
|
location: sites.location,
|
||||||
@ -3199,7 +3198,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
longitude: sites.longitude,
|
longitude: sites.longitude,
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.innerJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
|
.leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sites.location, location as "roccapiemonte" | "milano" | "roma"),
|
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)" });
|
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
|
const allGuards = await db
|
||||||
.select()
|
.select()
|
||||||
.from(guards)
|
.from(guards)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(guards.location, location as "roccapiemonte" | "milano" | "roma"),
|
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
|
// Calcola settimana corrente per calcolare ore settimanali
|
||||||
const [year, month, day] = date.split("-").map(Number);
|
const [year, month, day] = date.split("-").map(Number);
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.39",
|
"version": "1.0.40",
|
||||||
"lastUpdate": "2025-10-23T10:18:17.289Z",
|
"lastUpdate": "2025-10-23T10:49:40.822Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"version": "1.0.40",
|
||||||
|
"date": "2025-10-23",
|
||||||
|
"type": "patch",
|
||||||
|
"description": "Deployment automatico v1.0.40"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.39",
|
"version": "1.0.39",
|
||||||
"date": "2025-10-23",
|
"date": "2025-10-23",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user