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:
parent
6212b6b634
commit
a300d18489
8
.replit
8
.replit
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
<StatusBadge status={getStatusVariant(shift.status)}>
|
||||
{getStatusLabel(shift.status)}
|
||||
</StatusBadge>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
<StatusBadge status={site.isActive ? "active" : "inactive"}>
|
||||
{site.isActive ? "Attivo" : "Inattivo"}
|
||||
</StatusBadge>
|
||||
<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">
|
||||
|
||||
25
replit.md
25
replit.md
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user