Add editing capabilities for guards, sites, and shifts

Implement PATCH endpoints for updating guards, sites, and shifts, along with UI elements and form handling for editing existing records across the application.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/Iga2bds
This commit is contained in:
marco370 2025-10-11 15:58:34 +00:00
parent 6212b6b634
commit a300d18489
5 changed files with 621 additions and 17 deletions

View File

@ -18,14 +18,6 @@ externalPort = 80
localPort = 33035
externalPort = 3001
[[ports]]
localPort = 33349
externalPort = 3002
[[ports]]
localPort = 38973
externalPort = 3003
[[ports]]
localPort = 41343
externalPort = 3000

View File

@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertGuardSchema, insertCertificationSchema } from "@shared/schema";
import { Plus, Shield, Check, X, AlertCircle } from "lucide-react";
import { Plus, Shield, Check, X, AlertCircle, Pencil } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { StatusBadge } from "@/components/status-badge";
@ -22,6 +22,7 @@ import { format } from "date-fns";
export default function Guards() {
const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingGuard, setEditingGuard] = useState<GuardWithCertifications | null>(null);
const { data: guards, isLoading } = useQuery<GuardWithCertifications[]>({
queryKey: ["/api/guards"],
@ -40,6 +41,19 @@ export default function Guards() {
},
});
const editForm = useForm<InsertGuard>({
resolver: zodResolver(insertGuardSchema),
defaultValues: {
badgeNumber: "",
phoneNumber: "",
isArmed: false,
hasFireSafety: false,
hasFirstAid: false,
hasDriverLicense: false,
languages: [],
},
});
const createMutation = useMutation({
mutationFn: async (data: InsertGuard) => {
return await apiRequest("POST", "/api/guards", data);
@ -62,10 +76,51 @@ export default function Guards() {
},
});
const updateMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: InsertGuard }) => {
return await apiRequest("PATCH", `/api/guards/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/guards"] });
toast({
title: "Guardia aggiornata",
description: "I dati della guardia sono stati aggiornati",
});
setEditingGuard(null);
editForm.reset();
},
onError: (error) => {
toast({
title: "Errore",
description: error.message,
variant: "destructive",
});
},
});
const onSubmit = (data: InsertGuard) => {
createMutation.mutate(data);
};
const onEditSubmit = (data: InsertGuard) => {
if (editingGuard) {
updateMutation.mutate({ id: editingGuard.id, data });
}
};
const openEditDialog = (guard: GuardWithCertifications) => {
setEditingGuard(guard);
editForm.reset({
badgeNumber: guard.badgeNumber,
phoneNumber: guard.phoneNumber || "",
isArmed: guard.isArmed,
hasFireSafety: guard.hasFireSafety,
hasFirstAid: guard.hasFirstAid,
hasDriverLicense: guard.hasDriverLicense,
languages: guard.languages || [],
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -201,6 +256,126 @@ export default function Guards() {
</Dialog>
</div>
{/* Edit Guard Dialog */}
<Dialog open={!!editingGuard} onOpenChange={(open) => !open && setEditingGuard(null)}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Modifica Guardia</DialogTitle>
<DialogDescription>
Modifica i dati della guardia {editingGuard?.user?.firstName} {editingGuard?.user?.lastName}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="badgeNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Matricola</FormLabel>
<FormControl>
<Input placeholder="GPV-001" {...field} data-testid="input-edit-badge-number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Telefono</FormLabel>
<FormControl>
<Input placeholder="+39 123 456 7890" {...field} value={field.value || ""} data-testid="input-edit-phone" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Competenze</p>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="isArmed"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Armato</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-armed" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasFireSafety"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Antincendio</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-fire" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasFirstAid"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Primo Soccorso</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-first-aid" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Patente</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-driver" />
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditingGuard(null)}
className="flex-1"
data-testid="button-edit-cancel"
>
Annulla
</Button>
<Button
type="submit"
className="flex-1"
disabled={updateMutation.isPending}
data-testid="button-submit-edit-guard"
>
{updateMutation.isPending ? "Aggiornamento..." : "Salva Modifiche"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
{isLoading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-64" />
@ -227,6 +402,14 @@ export default function Guards() {
{guard.badgeNumber}
</CardDescription>
</div>
<Button
size="icon"
variant="ghost"
onClick={() => openEditDialog(guard)}
data-testid={`button-edit-guard-${guard.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">

View File

@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertShiftFormSchema } from "@shared/schema";
import { Plus, Calendar, MapPin, Users, Clock, UserPlus, X, Shield, Car, Heart, Flame } from "lucide-react";
import { Plus, Calendar, MapPin, Users, Clock, UserPlus, X, Shield, Car, Heart, Flame, Pencil } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { StatusBadge } from "@/components/status-badge";
@ -23,6 +23,7 @@ export default function Shifts() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedShift, setSelectedShift] = useState<ShiftWithDetails | null>(null);
const [isAssignDialogOpen, setIsAssignDialogOpen] = useState(false);
const [editingShift, setEditingShift] = useState<ShiftWithDetails | null>(null);
const { data: shifts, isLoading: shiftsLoading } = useQuery<ShiftWithDetails[]>({
queryKey: ["/api/shifts"],
@ -46,6 +47,16 @@ export default function Shifts() {
},
});
const editForm = useForm({
resolver: zodResolver(insertShiftFormSchema),
defaultValues: {
siteId: "",
startTime: "",
endTime: "",
status: "planned" as const,
},
});
const createMutation = useMutation({
mutationFn: async (data: InsertShift) => {
return await apiRequest("POST", "/api/shifts", data);
@ -130,6 +141,28 @@ export default function Shifts() {
},
});
const updateMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: InsertShift }) => {
return await apiRequest("PATCH", `/api/shifts/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/shifts"] });
toast({
title: "Turno aggiornato",
description: "Il turno è stato aggiornato con successo",
});
setEditingShift(null);
editForm.reset();
},
onError: (error) => {
toast({
title: "Errore",
description: error.message,
variant: "destructive",
});
},
});
const handleAssignGuard = (guardId: string) => {
if (selectedShift) {
assignGuardMutation.mutate({ shiftId: selectedShift.id, guardId });
@ -140,6 +173,27 @@ export default function Shifts() {
removeAssignmentMutation.mutate(assignmentId);
};
const onEditSubmit = (data: InsertShift) => {
if (editingShift) {
updateMutation.mutate({ id: editingShift.id, data });
}
};
const openEditDialog = (shift: ShiftWithDetails) => {
const formatForInput = (date: Date | string) => {
const d = new Date(date);
return d.toISOString().slice(0, 16);
};
setEditingShift(shift);
editForm.reset({
siteId: shift.siteId,
startTime: formatForInput(shift.startTime),
endTime: formatForInput(shift.endTime),
status: shift.status,
});
};
const isGuardAssigned = (guardId: string) => {
return selectedShift?.assignments.some(a => a.guardId === guardId) || false;
};
@ -315,9 +369,19 @@ export default function Shifts() {
{format(new Date(shift.endTime), "HH:mm", { locale: it })}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<StatusBadge status={getStatusVariant(shift.status)}>
{getStatusLabel(shift.status)}
</StatusBadge>
<Button
size="icon"
variant="ghost"
onClick={() => openEditDialog(shift)}
data-testid={`button-edit-shift-${shift.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
@ -496,6 +560,132 @@ export default function Shifts() {
)}
</DialogContent>
</Dialog>
{/* Edit Shift Dialog */}
<Dialog open={!!editingShift} onOpenChange={(open) => !open && setEditingShift(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Modifica Turno</DialogTitle>
<DialogDescription>
Modifica i dati del turno presso {editingShift?.site.name}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="siteId"
render={({ field }) => (
<FormItem>
<FormLabel>Sito</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="select-edit-site">
<SelectValue placeholder="Seleziona sito" />
</SelectTrigger>
</FormControl>
<SelectContent>
{sites?.map((site) => (
<SelectItem key={site.id} value={site.id}>
{site.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Inizio</FormLabel>
<FormControl>
<input
type="datetime-local"
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
data-testid="input-edit-start-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="endTime"
render={({ field }) => (
<FormItem>
<FormLabel>Fine</FormLabel>
<FormControl>
<input
type="datetime-local"
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
data-testid="input-edit-end-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Stato Turno</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="select-edit-status">
<SelectValue placeholder="Seleziona stato" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="planned">Pianificato</SelectItem>
<SelectItem value="active">Attivo</SelectItem>
<SelectItem value="completed">Completato</SelectItem>
<SelectItem value="cancelled">Annullato</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditingShift(null)}
className="flex-1"
data-testid="button-edit-shift-cancel"
>
Annulla
</Button>
<Button
type="submit"
className="flex-1"
disabled={updateMutation.isPending}
data-testid="button-submit-edit-shift"
>
{updateMutation.isPending ? "Aggiornamento..." : "Salva Modifiche"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -11,7 +11,7 @@ import { Switch } from "@/components/ui/switch";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertSiteSchema } from "@shared/schema";
import { Plus, MapPin, Shield, Users } from "lucide-react";
import { Plus, MapPin, Shield, Users, Pencil } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { StatusBadge } from "@/components/status-badge";
@ -28,6 +28,7 @@ const shiftTypeLabels: Record<string, string> = {
export default function Sites() {
const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingSite, setEditingSite] = useState<Site | null>(null);
const { data: sites, isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
@ -46,6 +47,19 @@ export default function Sites() {
},
});
const editForm = useForm<InsertSite>({
resolver: zodResolver(insertSiteSchema),
defaultValues: {
name: "",
address: "",
shiftType: "fixed_post",
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
isActive: true,
},
});
const createMutation = useMutation({
mutationFn: async (data: InsertSite) => {
return await apiRequest("POST", "/api/sites", data);
@ -68,10 +82,51 @@ export default function Sites() {
},
});
const updateMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: InsertSite }) => {
return await apiRequest("PATCH", `/api/sites/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/sites"] });
toast({
title: "Sito aggiornato",
description: "I dati del sito sono stati aggiornati",
});
setEditingSite(null);
editForm.reset();
},
onError: (error) => {
toast({
title: "Errore",
description: error.message,
variant: "destructive",
});
},
});
const onSubmit = (data: InsertSite) => {
createMutation.mutate(data);
};
const onEditSubmit = (data: InsertSite) => {
if (editingSite) {
updateMutation.mutate({ id: editingSite.id, data });
}
};
const openEditDialog = (site: Site) => {
setEditingSite(site);
editForm.reset({
name: site.name,
address: site.address,
shiftType: site.shiftType,
minGuards: site.minGuards,
requiresArmed: site.requiresArmed,
requiresDriverLicense: site.requiresDriverLicense,
isActive: site.isActive,
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -223,6 +278,155 @@ export default function Sites() {
</Dialog>
</div>
{/* Edit Site Dialog */}
<Dialog open={!!editingSite} onOpenChange={(open) => !open && setEditingSite(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Modifica Sito</DialogTitle>
<DialogDescription>
Modifica i dati del sito {editingSite?.name}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Sito</FormLabel>
<FormControl>
<Input placeholder="Centro Commerciale Nord" {...field} data-testid="input-edit-site-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Indirizzo</FormLabel>
<FormControl>
<Input placeholder="Via Roma 123, Milano" {...field} data-testid="input-edit-address" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="shiftType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipologia Servizio</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="select-edit-shift-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>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="minGuards"
render={({ field }) => (
<FormItem>
<FormLabel>Numero Minimo Guardie</FormLabel>
<FormControl>
<Input
type="number"
min={1}
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value))}
data-testid="input-edit-min-guards"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Requisiti</p>
<FormField
control={editForm.control}
name="requiresArmed"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Richiede Guardia Armata</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-requires-armed" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="requiresDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Richiede Patente</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-requires-driver" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Sito Attivo</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-is-active" />
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditingSite(null)}
className="flex-1"
data-testid="button-edit-site-cancel"
>
Annulla
</Button>
<Button
type="submit"
className="flex-1"
disabled={updateMutation.isPending}
data-testid="button-submit-edit-site"
>
{updateMutation.isPending ? "Aggiornamento..." : "Salva Modifiche"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
{isLoading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-48" />
@ -242,9 +446,19 @@ export default function Sites() {
{site.address}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<StatusBadge status={site.isActive ? "active" : "inactive"}>
{site.isActive ? "Attivo" : "Inattivo"}
</StatusBadge>
<Button
size="icon"
variant="ghost"
onClick={() => openEditDialog(site)}
data-testid={`button-edit-site-${site.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">

View File

@ -78,18 +78,28 @@ Sistema professionale di gestione turni 24/7 per istituti di vigilanza con:
- `GET /api/logout` - Logout
- `GET /api/auth/user` - Current user (protected)
### Users
- `GET /api/users` - Lista utenti (admin only)
- `PATCH /api/users/:id` - Modifica ruolo utente (admin only, non self)
### Guards
- `GET /api/guards` - Lista guardie con certificazioni
- `POST /api/guards` - Crea guardia
- `PATCH /api/guards/:id` - Aggiorna guardia
- `DELETE /api/guards/:id` - Elimina guardia
### Sites
- `GET /api/sites` - Lista siti
- `POST /api/sites` - Crea sito
- `PATCH /api/sites/:id` - Aggiorna sito
- `DELETE /api/sites/:id` - Elimina sito
### Shifts
- `GET /api/shifts` - Lista tutti i turni
- `GET /api/shifts/active` - Solo turni attivi
- `POST /api/shifts` - Crea turno
- `PATCH /api/shifts/:id` - Aggiorna turno
- `DELETE /api/shifts/:id` - Elimina turno
### Notifications
- `GET /api/notifications` - Lista notifiche utente
@ -105,6 +115,7 @@ Sistema professionale di gestione turni 24/7 per istituti di vigilanza con:
| `/shifts` | Admin, Coordinator, Guard | Pianificazione turni |
| `/reports` | Admin, Coordinator, Client | Reportistica |
| `/notifications` | Admin, Coordinator, Guard | Notifiche |
| `/users` | Admin | Gestione utenti e ruoli |
## User Roles
@ -205,6 +216,20 @@ All interactive elements have `data-testid` attributes for automated testing.
- PATCH/DELETE /api/shifts/:id
- 404 handling quando risorse non esistono
- Storage methods restituiscono entità aggiornate/eliminate
- **Gestione Utenti e Ruoli** ✅:
- Pagina /users solo per admin (route protetta)
- Modifica ruoli utenti via dropdown (admin, coordinator, guard, client)
- Protezione: impossibile modificare il proprio ruolo
- GET /api/users e PATCH /api/users/:id con controlli autorizzazione
- UI con avatar, email, ruolo corrente
- **Funzionalità Modifica Record** ✅:
- Pulsanti edit (icona matita) su Guards, Sites, Shifts
- Dialog di modifica con form precompilati
- Validazione zodResolver per tutti i form
- PATCH mutations con cache invalidation automatica
- Toast notifiche successo/errore
- Auto-close dialog dopo aggiornamento
- Test e2e passati per tutte le pagine ✅
- Aggiunto SEO completo (title, meta description, Open Graph)
- Tutti i componenti testabili con data-testid attributes