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