From a300d18489a164621e9dd941e93cc70dc1d1d5b5 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 11 Oct 2025 15:58:34 +0000 Subject: [PATCH] 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 --- .replit | 8 -- client/src/pages/guards.tsx | 185 +++++++++++++++++++++++++++++- client/src/pages/shifts.tsx | 198 +++++++++++++++++++++++++++++++- client/src/pages/sites.tsx | 222 +++++++++++++++++++++++++++++++++++- replit.md | 25 ++++ 5 files changed, 621 insertions(+), 17 deletions(-) diff --git a/.replit b/.replit index 9fb736e..03709f0 100644 --- a/.replit +++ b/.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 diff --git a/client/src/pages/guards.tsx b/client/src/pages/guards.tsx index 42c54d4..d9f62a1 100644 --- a/client/src/pages/guards.tsx +++ b/client/src/pages/guards.tsx @@ -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(null); const { data: guards, isLoading } = useQuery({ queryKey: ["/api/guards"], @@ -40,6 +41,19 @@ export default function Guards() { }, }); + const editForm = useForm({ + 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 (
@@ -201,6 +256,126 @@ export default function Guards() {
+ {/* Edit Guard Dialog */} + !open && setEditingGuard(null)}> + + + Modifica Guardia + + Modifica i dati della guardia {editingGuard?.user?.firstName} {editingGuard?.user?.lastName} + + +
+ + ( + + Matricola + + + + + + )} + /> + + ( + + Telefono + + + + + + )} + /> + +
+

Competenze

+
+ ( + + Armato + + + + + )} + /> + + ( + + Antincendio + + + + + )} + /> + + ( + + Primo Soccorso + + + + + )} + /> + + ( + + Patente + + + + + )} + /> +
+
+ +
+ + +
+ + +
+
+ {isLoading ? (
@@ -227,6 +402,14 @@ export default function Guards() { {guard.badgeNumber}
+
diff --git a/client/src/pages/shifts.tsx b/client/src/pages/shifts.tsx index 7645289..c862c44 100644 --- a/client/src/pages/shifts.tsx +++ b/client/src/pages/shifts.tsx @@ -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(null); const [isAssignDialogOpen, setIsAssignDialogOpen] = useState(false); + const [editingShift, setEditingShift] = useState(null); const { data: shifts, isLoading: shiftsLoading } = useQuery({ 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 })} - - {getStatusLabel(shift.status)} - +
+ + {getStatusLabel(shift.status)} + + +
@@ -496,6 +560,132 @@ export default function Shifts() { )} + + {/* Edit Shift Dialog */} + !open && setEditingShift(null)}> + + + Modifica Turno + + Modifica i dati del turno presso {editingShift?.site.name} + + +
+ + ( + + Sito + + + + )} + /> + +
+ ( + + Inizio + + field.onChange(e.target.value)} + data-testid="input-edit-start-time" + /> + + + + )} + /> + + ( + + Fine + + field.onChange(e.target.value)} + data-testid="input-edit-end-time" + /> + + + + )} + /> +
+ + ( + + Stato Turno + + + + )} + /> + +
+ + +
+ + +
+
); } diff --git a/client/src/pages/sites.tsx b/client/src/pages/sites.tsx index f2b4a63..9981e8e 100644 --- a/client/src/pages/sites.tsx +++ b/client/src/pages/sites.tsx @@ -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 = { export default function Sites() { const { toast } = useToast(); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingSite, setEditingSite] = useState(null); const { data: sites, isLoading } = useQuery({ queryKey: ["/api/sites"], @@ -46,6 +47,19 @@ export default function Sites() { }, }); + const editForm = useForm({ + 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 (
@@ -223,6 +278,155 @@ export default function Sites() {
+ {/* Edit Site Dialog */} + !open && setEditingSite(null)}> + + + Modifica Sito + + Modifica i dati del sito {editingSite?.name} + + +
+ + ( + + Nome Sito + + + + + + )} + /> + + ( + + Indirizzo + + + + + + )} + /> + + ( + + Tipologia Servizio + + + + )} + /> + + ( + + Numero Minimo Guardie + + field.onChange(parseInt(e.target.value))} + data-testid="input-edit-min-guards" + /> + + + + )} + /> + +
+

Requisiti

+ ( + + Richiede Guardia Armata + + + + + )} + /> + + ( + + Richiede Patente + + + + + )} + /> + + ( + + Sito Attivo + + + + + )} + /> +
+ +
+ + +
+ + +
+
+ {isLoading ? (
@@ -242,9 +446,19 @@ export default function Sites() { {site.address}
- - {site.isActive ? "Attivo" : "Inattivo"} - +
+ + {site.isActive ? "Attivo" : "Inattivo"} + + +
diff --git a/replit.md b/replit.md index 41b94ff..ab159b5 100644 --- a/replit.md +++ b/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