Compare commits

...

6 Commits

Author SHA1 Message Date
Marco Lanzara
dd468716d9 🚀 Release v1.0.7
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.7_20251017_080727.sql.gz
- Data: 2025-10-17 08:07:39
2025-10-17 08:07:39 +00:00
marco370
fdbcc3eee3 Add version number to the system header and implement planning page
Update UI with version number, implement planning page, and address issues with vehicle management and service page.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
2025-10-17 08:00:49 +00:00
marco370
7ab5ae65a7 Introduce an advanced planning page and improve existing planning features
Replaces the existing planning page with an advanced version, adds a new route and sidebar link for advanced planning, and refactors the planning page component to support new functionalities.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
2025-10-17 08:00:17 +00:00
marco370
5c6bf0ca4d Update vehicle management to include location selection
Modify the client-side vehicle management page to allow selection of vehicle location (site) during creation and editing, and adjust the form layout to accommodate the new field.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
2025-10-17 07:56:59 +00:00
marco370
500da807cf Add a new page for managing different types of security services
Implement the Services page with routing, navigation, and data fetching for service statistics.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
2025-10-17 07:55:17 +00:00
marco370
8237234fad Add current version number to the dashboard and specify testing environment
Add current version number to the dashboard UI and document that all tests are performed on the external server with local authentication.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/IdDfihe
2025-10-17 07:53:11 +00:00
9 changed files with 990 additions and 457 deletions

View File

@ -17,9 +17,11 @@ import Shifts from "@/pages/shifts";
import Reports from "@/pages/reports"; import Reports from "@/pages/reports";
import Notifications from "@/pages/notifications"; import Notifications from "@/pages/notifications";
import Users from "@/pages/users"; import Users from "@/pages/users";
import Planning from "@/pages/planning"; import AdvancedPlanning from "@/pages/advanced-planning";
import Vehicles from "@/pages/vehicles"; import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters"; import Parameters from "@/pages/parameters";
import Services from "@/pages/services";
import Planning from "@/pages/planning";
function Router() { function Router() {
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading } = useAuth();
@ -34,9 +36,11 @@ function Router() {
<Route path="/" component={Dashboard} /> <Route path="/" component={Dashboard} />
<Route path="/guards" component={Guards} /> <Route path="/guards" component={Guards} />
<Route path="/sites" component={Sites} /> <Route path="/sites" component={Sites} />
<Route path="/services" component={Services} />
<Route path="/vehicles" component={Vehicles} /> <Route path="/vehicles" component={Vehicles} />
<Route path="/shifts" component={Shifts} /> <Route path="/shifts" component={Shifts} />
<Route path="/planning" component={Planning} /> <Route path="/planning" component={Planning} />
<Route path="/advanced-planning" component={AdvancedPlanning} />
<Route path="/reports" component={Reports} /> <Route path="/reports" component={Reports} />
<Route path="/notifications" component={Notifications} /> <Route path="/notifications" component={Notifications} />
<Route path="/users" component={Users} /> <Route path="/users" component={Users} />

View File

@ -10,6 +10,7 @@ import {
UserCog, UserCog,
ClipboardList, ClipboardList,
Car, Car,
Briefcase,
} from "lucide-react"; } from "lucide-react";
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { import {
@ -48,6 +49,12 @@ const menuItems = [
icon: ClipboardList, icon: ClipboardList,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
{
title: "Pianificazione Avanzata",
url: "/advanced-planning",
icon: ClipboardList,
roles: ["admin", "coordinator"],
},
{ {
title: "Guardie", title: "Guardie",
url: "/guards", url: "/guards",
@ -60,6 +67,12 @@ const menuItems = [
icon: MapPin, icon: MapPin,
roles: ["admin", "coordinator", "client"], roles: ["admin", "coordinator", "client"],
}, },
{
title: "Servizi",
url: "/services",
icon: Briefcase,
roles: ["admin", "coordinator"],
},
{ {
title: "Parco Automezzi", title: "Parco Automezzi",
url: "/vehicles", url: "/vehicles",

View File

@ -0,0 +1,480 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { Calendar, GraduationCap, HeartPulse, PartyPopper, Plus, Trash2 } from "lucide-react";
import { format } from "date-fns";
import { it } from "date-fns/locale";
export default function PlanningPage() {
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("training");
return (
<div className="flex flex-col gap-6 p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</h1>
<p className="text-muted-foreground mt-1">
Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="training" data-testid="tab-training">
<GraduationCap className="h-4 w-4 mr-2" />
Formazione
</TabsTrigger>
<TabsTrigger value="absences" data-testid="tab-absences">
<HeartPulse className="h-4 w-4 mr-2" />
Assenze
</TabsTrigger>
<TabsTrigger value="holidays" data-testid="tab-holidays">
<PartyPopper className="h-4 w-4 mr-2" />
Festività
</TabsTrigger>
</TabsList>
<TabsContent value="training">
<TrainingTab />
</TabsContent>
<TabsContent value="absences">
<AbsencesTab />
</TabsContent>
<TabsContent value="holidays">
<HolidaysTab />
</TabsContent>
</Tabs>
</div>
);
}
function TrainingTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: courses = [], isLoading } = useQuery({
queryKey: ["/api/training-courses"],
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest("/api/training-courses", "POST", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
toast({ title: "Corso creato con successo" });
setIsCreateOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiRequest(`/api/training-courses/${id}`, "DELETE"),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
toast({ title: "Corso eliminato" });
},
});
if (isLoading) {
return <div className="text-center py-8">Caricamento corsi...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-course">
<Plus className="h-4 w-4 mr-2" />
Nuovo Corso
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuovo Corso di Formazione</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createMutation.mutate({
guardId: formData.get("guardId"),
courseName: formData.get("courseName"),
courseType: formData.get("courseType"),
scheduledDate: formData.get("scheduledDate"),
status: "scheduled",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="courseName">Nome Corso</Label>
<Input id="courseName" name="courseName" required data-testid="input-course-name" />
</div>
<div>
<Label htmlFor="courseType">Tipo</Label>
<Select name="courseType" required>
<SelectTrigger data-testid="select-course-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mandatory">Obbligatorio</SelectItem>
<SelectItem value="optional">Facoltativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-guard-id" />
</div>
<div>
<Label htmlFor="scheduledDate">Data Programmata</Label>
<Input id="scheduledDate" name="scheduledDate" type="date" required data-testid="input-scheduled-date" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-course">
{createMutation.isPending ? "Creazione..." : "Crea Corso"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{courses.map((course: any) => (
<Card key={course.id} data-testid={`card-course-${course.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{course.courseName}</CardTitle>
<CardDescription>
Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={course.courseType === "mandatory" ? "default" : "secondary"}>
{course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"}
</Badge>
<Badge variant={course.status === "completed" ? "default" : "secondary"}>
{course.status}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(course.id)}
data-testid={`button-delete-course-${course.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{courses.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessun corso di formazione programmato
</div>
)}
</div>
</div>
);
}
function AbsencesTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: absences = [], isLoading } = useQuery({
queryKey: ["/api/absences"],
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest("/api/absences", "POST", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
toast({ title: "Assenza registrata" });
setIsCreateOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiRequest(`/api/absences/${id}`, "DELETE"),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
toast({ title: "Assenza eliminata" });
},
});
if (isLoading) {
return <div className="text-center py-8">Caricamento assenze...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-absence">
<Plus className="h-4 w-4 mr-2" />
Nuova Assenza
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Registra Assenza</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createMutation.mutate({
guardId: formData.get("guardId"),
type: formData.get("type"),
startDate: formData.get("startDate"),
endDate: formData.get("endDate"),
notes: formData.get("notes"),
isApproved: false,
needsSubstitute: true,
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-absence-guard-id" />
</div>
<div>
<Label htmlFor="type">Tipo Assenza</Label>
<Select name="type" required>
<SelectTrigger data-testid="select-absence-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sick_leave">Malattia</SelectItem>
<SelectItem value="vacation">Ferie</SelectItem>
<SelectItem value="personal_leave">Permesso</SelectItem>
<SelectItem value="injury">Infortunio</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="startDate">Data Inizio</Label>
<Input id="startDate" name="startDate" type="date" required data-testid="input-start-date" />
</div>
<div>
<Label htmlFor="endDate">Data Fine</Label>
<Input id="endDate" name="endDate" type="date" required data-testid="input-end-date" />
</div>
<div>
<Label htmlFor="notes">Note</Label>
<Input id="notes" name="notes" data-testid="input-absence-notes" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-absence">
{createMutation.isPending ? "Registrazione..." : "Registra Assenza"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{absences.map((absence: any) => (
<Card key={absence.id} data-testid={`card-absence-${absence.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">
{absence.type === "sick_leave" && "Malattia"}
{absence.type === "vacation" && "Ferie"}
{absence.type === "personal_leave" && "Permesso"}
{absence.type === "injury" && "Infortunio"}
</CardTitle>
<CardDescription>
Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "}
{format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={absence.isApproved ? "default" : "secondary"}>
{absence.isApproved ? "Approvata" : "In attesa"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(absence.id)}
data-testid={`button-delete-absence-${absence.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{absences.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessuna assenza registrata
</div>
)}
</div>
</div>
);
}
function HolidaysTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const currentYear = new Date().getFullYear();
const { data: holidays = [], isLoading } = useQuery({
queryKey: ["/api/holidays", currentYear],
queryFn: () => fetch(`/api/holidays?year=${currentYear}`).then((r) => r.json()),
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest("/api/holidays", "POST", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] });
toast({ title: "Festività creata" });
setIsCreateOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiRequest(`/api/holidays/${id}`, "DELETE"),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] });
toast({ title: "Festività eliminata" });
},
});
if (isLoading) {
return <div className="text-center py-8">Caricamento festività...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-holiday">
<Plus className="h-4 w-4 mr-2" />
Nuova Festività
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuova Festività</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const dateValue = formData.get("date") as string;
createMutation.mutate({
name: formData.get("name"),
date: dateValue,
year: new Date(dateValue).getFullYear(),
isNational: formData.get("isNational") === "true",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="name">Nome Festività</Label>
<Input id="name" name="name" required data-testid="input-holiday-name" placeholder="es. Natale, 1° Maggio..." />
</div>
<div>
<Label htmlFor="date">Data</Label>
<Input id="date" name="date" type="date" required data-testid="input-holiday-date" />
</div>
<div>
<Label htmlFor="isNational">Tipo</Label>
<Select name="isNational" defaultValue="true">
<SelectTrigger data-testid="select-holiday-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Nazionale</SelectItem>
<SelectItem value="false">Regionale</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-holiday">
{createMutation.isPending ? "Creazione..." : "Crea Festività"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{holidays.map((holiday: any) => (
<Card key={holiday.id} data-testid={`card-holiday-${holiday.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{holiday.name}</CardTitle>
<CardDescription>
{format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={holiday.isNational ? "default" : "secondary"}>
{holiday.isNational ? "Nazionale" : "Regionale"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(holiday.id)}
data-testid={`button-delete-holiday-${holiday.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{holidays.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessuna festività configurata per {currentYear}
</div>
)}
</div>
</div>
);
}

View File

@ -10,6 +10,7 @@ import { ShiftWithDetails, GuardWithCertifications, Site } from "@shared/schema"
import { formatDistanceToNow, format } from "date-fns"; import { formatDistanceToNow, format } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import versionInfo from "../../../version.json";
export default function Dashboard() { export default function Dashboard() {
const { user } = useAuth(); const { user } = useAuth();
@ -67,6 +68,9 @@ export default function Dashboard() {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-semibold mb-2">Dashboard Operativa</h1> <h1 className="text-3xl font-semibold mb-2">Dashboard Operativa</h1>
<p className="text-sm text-muted-foreground/70 mb-2" data-testid="text-version">
v{versionInfo.version}
</p>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Benvenuto, {user?.firstName} {user?.lastName} Benvenuto, {user?.firstName} {user?.lastName}
</p> </p>

View File

@ -1,480 +1,270 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import { Vehicle, Site, ShiftWithDetails } from "@shared/schema";
Dialog, import { Building2, Car, MapPin, Calendar } from "lucide-react";
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { Calendar, GraduationCap, HeartPulse, PartyPopper, Plus, Trash2 } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
export default function PlanningPage() { const locationLabels = {
const { toast } = useToast(); all: "Tutte le Sedi",
const [activeTab, setActiveTab] = useState("training"); roccapiemonte: "Roccapiemonte",
milano: "Milano",
roma: "Roma"
} as const;
const vehicleStatusLabels = {
available: "Disponibile",
in_use: "In uso",
maintenance: "In manutenzione",
out_of_service: "Fuori servizio",
};
const vehicleStatusColors = {
available: "bg-green-500/10 text-green-500 border-green-500/20",
in_use: "bg-blue-500/10 text-blue-500 border-blue-500/20",
maintenance: "bg-orange-500/10 text-orange-500 border-orange-500/20",
out_of_service: "bg-red-500/10 text-red-500 border-red-500/20",
};
export default function Planning() {
const [selectedLocation, setSelectedLocation] = useState<string>("all");
const { data: vehicles = [], isLoading: loadingVehicles } = useQuery<Vehicle[]>({
queryKey: ["/api/vehicles"],
});
const { data: sites = [], isLoading: loadingSites } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
const { data: shifts = [], isLoading: loadingShifts } = useQuery<ShiftWithDetails[]>({
queryKey: ["/api/shifts/active"],
});
// Filter by location
const filteredVehicles = selectedLocation === "all"
? vehicles
: vehicles.filter(v => v.location === selectedLocation);
const filteredSites = selectedLocation === "all"
? sites
: sites.filter(s => s.location === selectedLocation);
const filteredShifts = selectedLocation === "all"
? shifts
: shifts.filter(s => {
const site = sites.find(site => site.id === s.siteId);
return site?.location === selectedLocation;
});
const isLoading = loadingVehicles || loadingSites || loadingShifts;
// Calculate stats
const availableVehicles = filteredVehicles.filter(v => v.status === "available").length;
const inUseVehicles = filteredVehicles.filter(v => v.status === "in_use").length;
const activeSites = filteredSites.filter(s => s.isActive).length;
const activeShifts = filteredShifts.filter(s => s.status === "active").length;
return ( return (
<div className="flex flex-col gap-6 p-8"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</h1> <h1 className="text-3xl font-semibold mb-2">Pianificazione Operativa</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground">
Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni Vista per sede con gestione automezzi e turni attivi
</p> </p>
</div> </div>
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-muted-foreground" />
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
<SelectTrigger className="w-[200px]" data-testid="select-location">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tutte le Sedi</SelectItem>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> {isLoading ? (
<TabsList className="grid w-full grid-cols-3"> <div className="text-center py-8 text-muted-foreground">Caricamento dati...</div>
<TabsTrigger value="training" data-testid="tab-training"> ) : (
<GraduationCap className="h-4 w-4 mr-2" /> <>
Formazione {/* Stats Summary */}
</TabsTrigger> <div className="grid gap-4 md:grid-cols-4">
<TabsTrigger value="absences" data-testid="tab-absences"> <Card>
<HeartPulse className="h-4 w-4 mr-2" /> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
Assenze <CardTitle className="text-sm font-medium">Turni Attivi</CardTitle>
</TabsTrigger> <Calendar className="h-4 w-4 text-muted-foreground" />
<TabsTrigger value="holidays" data-testid="tab-holidays"> </CardHeader>
<PartyPopper className="h-4 w-4 mr-2" /> <CardContent>
Festività <div className="text-2xl font-bold" data-testid="text-active-shifts">{activeShifts}</div>
</TabsTrigger> <p className="text-xs text-muted-foreground">
</TabsList> {locationLabels[selectedLocation as keyof typeof locationLabels]}
</p>
</CardContent>
</Card>
<TabsContent value="training"> <Card>
<TrainingTab /> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
</TabsContent> <CardTitle className="text-sm font-medium">Siti Attivi</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-active-sites">{activeSites}</div>
<p className="text-xs text-muted-foreground">
su {filteredSites.length} totali
</p>
</CardContent>
</Card>
<TabsContent value="absences"> <Card>
<AbsencesTab /> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
</TabsContent> <CardTitle className="text-sm font-medium">Automezzi Disponibili</CardTitle>
<Car className="h-4 w-4 text-muted-foreground" />
<TabsContent value="holidays"> </CardHeader>
<HolidaysTab /> <CardContent>
</TabsContent> <div className="text-2xl font-bold text-green-500" data-testid="text-available-vehicles">
</Tabs> {availableVehicles}
</div>
);
}
function TrainingTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: courses = [], isLoading } = useQuery({
queryKey: ["/api/training-courses"],
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest("/api/training-courses", "POST", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
toast({ title: "Corso creato con successo" });
setIsCreateOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiRequest(`/api/training-courses/${id}`, "DELETE"),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
toast({ title: "Corso eliminato" });
},
});
if (isLoading) {
return <div className="text-center py-8">Caricamento corsi...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-course">
<Plus className="h-4 w-4 mr-2" />
Nuovo Corso
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuovo Corso di Formazione</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createMutation.mutate({
guardId: formData.get("guardId"),
courseName: formData.get("courseName"),
courseType: formData.get("courseType"),
scheduledDate: formData.get("scheduledDate"),
status: "scheduled",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="courseName">Nome Corso</Label>
<Input id="courseName" name="courseName" required data-testid="input-course-name" />
</div> </div>
<div> <p className="text-xs text-muted-foreground">
<Label htmlFor="courseType">Tipo</Label> su {filteredVehicles.length} totali
<Select name="courseType" required> </p>
<SelectTrigger data-testid="select-course-type"> </CardContent>
<SelectValue placeholder="Seleziona tipo" /> </Card>
</SelectTrigger>
<SelectContent>
<SelectItem value="mandatory">Obbligatorio</SelectItem>
<SelectItem value="optional">Facoltativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-guard-id" />
</div>
<div>
<Label htmlFor="scheduledDate">Data Programmata</Label>
<Input id="scheduledDate" name="scheduledDate" type="date" required data-testid="input-scheduled-date" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-course">
{createMutation.isPending ? "Creazione..." : "Crea Corso"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4"> <Card>
{courses.map((course: any) => ( <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Card key={course.id} data-testid={`card-course-${course.id}`}> <CardTitle className="text-sm font-medium">Automezzi in Uso</CardTitle>
<CardHeader> <Car className="h-4 w-4 text-muted-foreground" />
<div className="flex items-start justify-between"> </CardHeader>
<div> <CardContent>
<CardTitle className="text-lg">{course.courseName}</CardTitle> <div className="text-2xl font-bold text-blue-500" data-testid="text-inuse-vehicles">
<CardDescription> {inUseVehicles}
Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"}
</CardDescription>
</div> </div>
<div className="flex gap-2 items-center"> <p className="text-xs text-muted-foreground">
<Badge variant={course.courseType === "mandatory" ? "default" : "secondary"}> assegnati a turni
{course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"} </p>
</Badge> </CardContent>
<Badge variant={course.status === "completed" ? "default" : "secondary"}> </Card>
{course.status}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(course.id)}
data-testid={`button-delete-course-${course.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{courses.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessun corso di formazione programmato
</div> </div>
)}
</div>
</div>
);
}
function AbsencesTab() { {/* Vehicles List */}
const { toast } = useToast(); <Card>
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: absences = [], isLoading } = useQuery({
queryKey: ["/api/absences"],
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest("/api/absences", "POST", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
toast({ title: "Assenza registrata" });
setIsCreateOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiRequest(`/api/absences/${id}`, "DELETE"),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
toast({ title: "Assenza eliminata" });
},
});
if (isLoading) {
return <div className="text-center py-8">Caricamento assenze...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-absence">
<Plus className="h-4 w-4 mr-2" />
Nuova Assenza
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Registra Assenza</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createMutation.mutate({
guardId: formData.get("guardId"),
type: formData.get("type"),
startDate: formData.get("startDate"),
endDate: formData.get("endDate"),
notes: formData.get("notes"),
isApproved: false,
needsSubstitute: true,
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-absence-guard-id" />
</div>
<div>
<Label htmlFor="type">Tipo Assenza</Label>
<Select name="type" required>
<SelectTrigger data-testid="select-absence-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sick_leave">Malattia</SelectItem>
<SelectItem value="vacation">Ferie</SelectItem>
<SelectItem value="personal_leave">Permesso</SelectItem>
<SelectItem value="injury">Infortunio</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="startDate">Data Inizio</Label>
<Input id="startDate" name="startDate" type="date" required data-testid="input-start-date" />
</div>
<div>
<Label htmlFor="endDate">Data Fine</Label>
<Input id="endDate" name="endDate" type="date" required data-testid="input-end-date" />
</div>
<div>
<Label htmlFor="notes">Note</Label>
<Input id="notes" name="notes" data-testid="input-absence-notes" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-absence">
{createMutation.isPending ? "Registrazione..." : "Registra Assenza"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{absences.map((absence: any) => (
<Card key={absence.id} data-testid={`card-absence-${absence.id}`}>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <CardTitle>Automezzi - {locationLabels[selectedLocation as keyof typeof locationLabels]}</CardTitle>
<div> <CardDescription>
<CardTitle className="text-lg"> Stato e disponibilità automezzi per sede selezionata
{absence.type === "sick_leave" && "Malattia"} </CardDescription>
{absence.type === "vacation" && "Ferie"}
{absence.type === "personal_leave" && "Permesso"}
{absence.type === "injury" && "Infortunio"}
</CardTitle>
<CardDescription>
Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "}
{format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={absence.isApproved ? "default" : "secondary"}>
{absence.isApproved ? "Approvata" : "In attesa"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(absence.id)}
data-testid={`button-delete-absence-${absence.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader> </CardHeader>
</Card> <CardContent>
))} <div className="space-y-4">
{absences.length === 0 && ( {filteredVehicles.map((vehicle) => (
<div className="text-center py-8 text-muted-foreground"> <div
Nessuna assenza registrata key={vehicle.id}
</div> className="flex items-center justify-between p-4 rounded-lg border bg-card hover-elevate"
)} data-testid={`card-vehicle-${vehicle.id}`}
</div> >
</div> <div className="flex items-center gap-4">
); <div className="p-2 rounded-lg bg-muted">
} <Car className="h-6 w-6" />
</div>
function HolidaysTab() { <div>
const { toast } = useToast(); <p className="font-semibold" data-testid={`text-vehicle-${vehicle.id}`}>
const [isCreateOpen, setIsCreateOpen] = useState(false); {vehicle.brand} {vehicle.model}
const currentYear = new Date().getFullYear(); </p>
<p className="text-sm text-muted-foreground">
const { data: holidays = [], isLoading } = useQuery({ {vehicle.licensePlate}
queryKey: ["/api/holidays", currentYear], {vehicle.year && ` • Anno ${vehicle.year}`}
queryFn: () => fetch(`/api/holidays?year=${currentYear}`).then((r) => r.json()), </p>
}); </div>
</div>
const createMutation = useMutation({ <div className="flex items-center gap-3">
mutationFn: (data: any) => apiRequest("/api/holidays", "POST", data), <Badge variant="outline" className="capitalize">
onSuccess: () => { {locationLabels[vehicle.location as keyof typeof locationLabels]}
queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] }); </Badge>
toast({ title: "Festività creata" }); <Badge
setIsCreateOpen(false); variant="outline"
}, className={vehicleStatusColors[vehicle.status]}
}); data-testid={`badge-status-${vehicle.id}`}
>
const deleteMutation = useMutation({ {vehicleStatusLabels[vehicle.status]}
mutationFn: (id: string) => apiRequest(`/api/holidays/${id}`, "DELETE"), </Badge>
onSuccess: () => { </div>
queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] }); </div>
toast({ title: "Festività eliminata" }); ))}
}, {filteredVehicles.length === 0 && (
}); <div className="text-center py-8 text-muted-foreground">
<Car className="h-12 w-12 mx-auto mb-2 opacity-50" />
if (isLoading) { <p>Nessun automezzo disponibile per questa sede</p>
return <div className="text-center py-8">Caricamento festività...</div>; </div>
} )}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-holiday">
<Plus className="h-4 w-4 mr-2" />
Nuova Festività
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuova Festività</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const dateValue = formData.get("date") as string;
createMutation.mutate({
name: formData.get("name"),
date: dateValue,
year: new Date(dateValue).getFullYear(),
isNational: formData.get("isNational") === "true",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="name">Nome Festività</Label>
<Input id="name" name="name" required data-testid="input-holiday-name" placeholder="es. Natale, 1° Maggio..." />
</div>
<div>
<Label htmlFor="date">Data</Label>
<Input id="date" name="date" type="date" required data-testid="input-holiday-date" />
</div>
<div>
<Label htmlFor="isNational">Tipo</Label>
<Select name="isNational" defaultValue="true">
<SelectTrigger data-testid="select-holiday-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Nazionale</SelectItem>
<SelectItem value="false">Regionale</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
<DialogFooter> </CardContent>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-holiday"> </Card>
{createMutation.isPending ? "Creazione..." : "Crea Festività"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4"> {/* Active Shifts */}
{holidays.map((holiday: any) => ( <Card>
<Card key={holiday.id} data-testid={`card-holiday-${holiday.id}`}>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <CardTitle>Turni Attivi - {locationLabels[selectedLocation as keyof typeof locationLabels]}</CardTitle>
<div> <CardDescription>
<CardTitle className="text-lg">{holiday.name}</CardTitle> Turni in corso per sede selezionata
<CardDescription> </CardDescription>
{format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={holiday.isNational ? "default" : "secondary"}>
{holiday.isNational ? "Nazionale" : "Regionale"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(holiday.id)}
data-testid={`button-delete-holiday-${holiday.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent>
<div className="space-y-4">
{filteredShifts.filter(s => s.status === "active").map((shift) => {
const site = sites.find(s => s.id === shift.siteId);
return (
<div
key={shift.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card hover-elevate"
data-testid={`card-shift-${shift.id}`}
>
<div className="flex items-center gap-4">
<div className="p-2 rounded-lg bg-muted">
<Calendar className="h-6 w-6" />
</div>
<div>
<p className="font-semibold" data-testid={`text-shift-site-${shift.id}`}>
{site?.name || "Sito sconosciuto"}
</p>
<p className="text-sm text-muted-foreground">
{format(new Date(shift.startTime), "HH:mm", { locale: it })} -
{format(new Date(shift.endTime), "HH:mm", { locale: it })}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="outline" className="capitalize">
{site && locationLabels[site.location as keyof typeof locationLabels]}
</Badge>
<Badge variant="default">
Attivo
</Badge>
</div>
</div>
);
})}
{filteredShifts.filter(s => s.status === "active").length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nessun turno attivo per questa sede</p>
</div>
)}
</div>
</CardContent>
</Card> </Card>
))} </>
{holidays.length === 0 && ( )}
<div className="text-center py-8 text-muted-foreground">
Nessuna festività configurata per {currentYear}
</div>
)}
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,189 @@
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Site } from "@shared/schema";
import { Building2, Shield, Eye, MapPin, Zap } from "lucide-react";
const serviceTypeInfo = {
fixed_post: {
label: "Presidio Fisso",
description: "Guardia fissa presso una struttura",
icon: Building2,
color: "bg-blue-500/10 text-blue-500 border-blue-500/20"
},
patrol: {
label: "Pattugliamento",
description: "Ronde e controlli su area",
icon: Eye,
color: "bg-green-500/10 text-green-500 border-green-500/20"
},
night_inspection: {
label: "Ispettorato Notturno",
description: "Controlli notturni programmati",
icon: Shield,
color: "bg-purple-500/10 text-purple-500 border-purple-500/20"
},
quick_response: {
label: "Pronto Intervento",
description: "Intervento rapido su chiamata",
icon: Zap,
color: "bg-orange-500/10 text-orange-500 border-orange-500/20"
}
} as const;
export default function Services() {
const { data: sites = [], isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
// Calculate statistics per service type
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
const sitesForType = sites.filter(s => s.shiftType === type);
acc[type] = {
total: sitesForType.length,
active: sitesForType.filter(s => s.isActive).length,
requiresArmed: sitesForType.filter(s => s.requiresArmed).length,
requiresDriver: sitesForType.filter(s => s.requiresDriverLicense).length,
};
return acc;
}, {} as Record<string, any>);
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
<p className="text-muted-foreground">
Panoramica tipologie di servizio e relative configurazioni
</p>
</div>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Caricamento servizi...</div>
) : (
<div className="grid gap-6 md:grid-cols-2">
{Object.entries(serviceTypeInfo).map(([type, info]) => {
const Icon = info.icon;
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
return (
<Card key={type} data-testid={`card-service-${type}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${info.color}`}>
<Icon className="h-6 w-6" />
</div>
<div>
<CardTitle className="text-xl">{info.label}</CardTitle>
<CardDescription className="mt-1">{info.description}</CardDescription>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Siti Totali</p>
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}>
{stat.total}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Attivi</p>
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
{stat.active}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Armati</p>
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}>
{stat.requiresArmed}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Patente</p>
<p className="text-lg font-semibold" data-testid={`text-driver-${type}`}>
{stat.requiresDriver}
</p>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma
</Badge>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
<Card>
<CardHeader>
<CardTitle>Informazioni Tipologie Servizio</CardTitle>
<CardDescription>Caratteristiche e utilizzo delle diverse tipologie</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<Building2 className="h-5 w-5 text-blue-500 mt-0.5" />
<div>
<h4 className="font-semibold">Presidio Fisso</h4>
<p className="text-sm text-muted-foreground mt-1">
Utilizzato per siti che richiedono sorveglianza continua con presenza fissa delle guardie.
Ideale per banche, musei, uffici pubblici.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<Eye className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<h4 className="font-semibold">Pattugliamento</h4>
<p className="text-sm text-muted-foreground mt-1">
Servizio di ronde mobili su area estesa. Le guardie effettuano controlli periodici
seguendo percorsi predefiniti. Richiede spesso patente di guida.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<Shield className="h-5 w-5 text-purple-500 mt-0.5" />
<div>
<h4 className="font-semibold">Ispettorato Notturno</h4>
<p className="text-sm text-muted-foreground mt-1">
Controlli specifici durante le ore notturne. Prevede verifiche programmate
di sicurezza e aperture/chiusure di strutture.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<Zap className="h-5 w-5 text-orange-500 mt-0.5" />
<div>
<h4 className="font-semibold">Pronto Intervento</h4>
<p className="text-sm text-muted-foreground mt-1">
Servizio di intervento rapido su chiamata. Le guardie devono essere disponibili
per interventi urgenti, spesso armati e con veicolo dedicato.
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -109,6 +109,7 @@ export default function Vehicles() {
brand: "", brand: "",
model: "", model: "",
vehicleType: "car", vehicleType: "car",
location: "roccapiemonte",
year: undefined, year: undefined,
assignedGuardId: null, assignedGuardId: null,
status: "available", status: "available",
@ -213,6 +214,7 @@ export default function Vehicles() {
brand: vehicle.brand, brand: vehicle.brand,
model: vehicle.model, model: vehicle.model,
vehicleType: vehicle.vehicleType, vehicleType: vehicle.vehicleType,
location: vehicle.location,
year: vehicle.year ?? undefined, year: vehicle.year ?? undefined,
assignedGuardId: vehicle.assignedGuardId, assignedGuardId: vehicle.assignedGuardId,
status: vehicle.status, status: vehicle.status,
@ -370,7 +372,7 @@ export default function Vehicles() {
</DialogHeader> </DialogHeader>
<Form {...createForm}> <Form {...createForm}>
<form onSubmit={createForm.handleSubmit((data) => createVehicleMutation.mutate(data))} className="space-y-4"> <form onSubmit={createForm.handleSubmit((data) => createVehicleMutation.mutate(data))} className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
<FormField <FormField
control={createForm.control} control={createForm.control}
name="licensePlate" name="licensePlate"
@ -384,6 +386,28 @@ export default function Vehicles() {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={createForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-create-location">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona sede" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={createForm.control} control={createForm.control}
name="vehicleType" name="vehicleType"
@ -575,7 +599,7 @@ export default function Vehicles() {
</DialogHeader> </DialogHeader>
<Form {...editForm}> <Form {...editForm}>
<form onSubmit={editForm.handleSubmit((data) => selectedVehicle && updateVehicleMutation.mutate({ id: selectedVehicle.id, data }))} className="space-y-4"> <form onSubmit={editForm.handleSubmit((data) => selectedVehicle && updateVehicleMutation.mutate({ id: selectedVehicle.id, data }))} className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
<FormField <FormField
control={editForm.control} control={editForm.control}
name="licensePlate" name="licensePlate"
@ -589,6 +613,28 @@ export default function Vehicles() {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={editForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-edit-location">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona sede" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={editForm.control} control={editForm.control}
name="vehicleType" name="vehicleType"

View File

@ -8,6 +8,7 @@ VigilanzaTurni is a professional 24/7 shift management system designed for secur
- Dark mode di default - Dark mode di default
- Design operativo e funzionale (non decorativo) - Design operativo e funzionale (non decorativo)
- Focus su efficienza e densità informativa - Focus su efficienza e densità informativa
- **Testing**: Tutti i test vengono eseguiti ESCLUSIVAMENTE sul server esterno (vt.alfacom.it) con autenticazione locale (non Replit Auth)
## System Architecture ## System Architecture

View File

@ -1,7 +1,13 @@
{ {
"version": "1.0.6", "version": "1.0.7",
"lastUpdate": "2025-10-17T07:44:09.569Z", "lastUpdate": "2025-10-17T08:07:39.479Z",
"changelog": [ "changelog": [
{
"version": "1.0.7",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.7"
},
{ {
"version": "1.0.6", "version": "1.0.6",
"date": "2025-10-17", "date": "2025-10-17",