From b3d04413068bbd151cac67b2b20ad6165f2d20d9 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 11 Oct 2025 11:03:43 +0000 Subject: [PATCH] Add functionality to assign and remove guards from shifts Implement API endpoints and UI components for managing shift assignments, including guard certifications and a new assignment removal mutation. 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/9lvKVew --- .replit | 4 + client/src/pages/shifts.tsx | 252 ++++++++++++++++++++++++++++++++++-- server/routes.ts | 10 ++ server/storage.ts | 6 + 4 files changed, 261 insertions(+), 11 deletions(-) diff --git a/.replit b/.replit index 03709f0..5fda2a5 100644 --- a/.replit +++ b/.replit @@ -18,6 +18,10 @@ externalPort = 80 localPort = 33035 externalPort = 3001 +[[ports]] +localPort = 38805 +externalPort = 3002 + [[ports]] localPort = 41343 externalPort = 3000 diff --git a/client/src/pages/shifts.tsx b/client/src/pages/shifts.tsx index ad7da5a..7645289 100644 --- a/client/src/pages/shifts.tsx +++ b/client/src/pages/shifts.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { ShiftWithDetails, InsertShift, Site, Guard } from "@shared/schema"; +import { ShiftWithDetails, InsertShift, Site, GuardWithCertifications } from "@shared/schema"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; @@ -9,17 +9,20 @@ 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 } from "lucide-react"; +import { Plus, Calendar, MapPin, Users, Clock, UserPlus, X, Shield, Car, Heart, Flame } from "lucide-react"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { StatusBadge } from "@/components/status-badge"; import { Skeleton } from "@/components/ui/skeleton"; import { format } from "date-fns"; import { it } from "date-fns/locale"; +import { Badge } from "@/components/ui/badge"; export default function Shifts() { const { toast } = useToast(); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [selectedShift, setSelectedShift] = useState(null); + const [isAssignDialogOpen, setIsAssignDialogOpen] = useState(false); const { data: shifts, isLoading: shiftsLoading } = useQuery({ queryKey: ["/api/shifts"], @@ -29,6 +32,10 @@ export default function Shifts() { queryKey: ["/api/sites"], }); + const { data: guards } = useQuery({ + queryKey: ["/api/guards"], + }); + const form = useForm({ resolver: zodResolver(insertShiftFormSchema), defaultValues: { @@ -65,6 +72,89 @@ export default function Shifts() { createMutation.mutate(data); }; + const assignGuardMutation = useMutation({ + mutationFn: async ({ shiftId, guardId }: { shiftId: string; guardId: string }) => { + return await apiRequest("POST", "/api/shift-assignments", { shiftId, guardId }); + }, + onSuccess: async () => { + // Invalidate and wait for refetch to complete + await queryClient.refetchQueries({ queryKey: ["/api/shifts"] }); + // Now get the fresh data + const updatedShifts = queryClient.getQueryData(["/api/shifts"]); + if (selectedShift && updatedShifts) { + const updatedShift = updatedShifts.find(s => s.id === selectedShift.id); + if (updatedShift) { + setSelectedShift(updatedShift); + } + } + toast({ + title: "Guardia assegnata", + description: "La guardia è stata assegnata al turno con successo", + }); + }, + onError: (error) => { + toast({ + title: "Errore", + description: error.message, + variant: "destructive", + }); + }, + }); + + const removeAssignmentMutation = useMutation({ + mutationFn: async (assignmentId: string) => { + return await apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`); + }, + onSuccess: async () => { + // Invalidate and wait for refetch to complete + await queryClient.refetchQueries({ queryKey: ["/api/shifts"] }); + // Now get the fresh data + const updatedShifts = queryClient.getQueryData(["/api/shifts"]); + if (selectedShift && updatedShifts) { + const updatedShift = updatedShifts.find(s => s.id === selectedShift.id); + if (updatedShift) { + setSelectedShift(updatedShift); + } + } + toast({ + title: "Assegnazione rimossa", + description: "La guardia è stata rimossa dal turno", + }); + }, + onError: (error) => { + toast({ + title: "Errore", + description: error.message, + variant: "destructive", + }); + }, + }); + + const handleAssignGuard = (guardId: string) => { + if (selectedShift) { + assignGuardMutation.mutate({ shiftId: selectedShift.id, guardId }); + } + }; + + const handleRemoveAssignment = (assignmentId: string) => { + removeAssignmentMutation.mutate(assignmentId); + }; + + const isGuardAssigned = (guardId: string) => { + return selectedShift?.assignments.some(a => a.guardId === guardId) || false; + }; + + const canGuardBeAssigned = (guard: GuardWithCertifications) => { + if (!selectedShift) return false; + const site = sites?.find(s => s.id === selectedShift.siteId); + if (!site) return true; + + if (site.requiresArmed && !guard.isArmed) return false; + if (site.requiresDriverLicense && !guard.hasDriverLicense) return false; + + return true; + }; + const getStatusLabel = (status: string) => { const labels: Record = { planned: "Pianificato", @@ -231,22 +321,50 @@ export default function Shifts() { -
- - - {shift.assignments.length > 0 - ? `${shift.assignments.length} guardie assegnate` - : "Nessuna guardia assegnata"} - +
+
+ + + {shift.assignments.length > 0 + ? `${shift.assignments.length} guardie assegnate` + : "Nessuna guardia assegnata"} + +
+ {shift.status === "planned" && ( + + )}
{shift.assignments.length > 0 && (
{shift.assignments.map((assignment) => (
- {assignment.guard.user?.firstName} {assignment.guard.user?.lastName} + + {assignment.guard.user?.firstName} {assignment.guard.user?.lastName} + + {shift.status === "planned" && ( + + )}
))}
@@ -266,6 +384,118 @@ export default function Shifts() { )} + + {/* Assignment Dialog */} + + + + Assegna Guardie al Turno + + {selectedShift && ( + <> + {selectedShift.site.name} -{" "} + {format(new Date(selectedShift.startTime), "dd/MM/yyyy HH:mm", { locale: it })} + + )} + + + + {selectedShift && ( +
+ {/* Site Requirements */} +
+

Requisiti Sito

+
+ {selectedShift.site.requiresArmed && ( + + + Armato richiesto + + )} + {selectedShift.site.requiresDriverLicense && ( + + + Patente richiesta + + )} + + Min. {selectedShift.site.minGuards} guardie + +
+
+ + {/* Guards List */} +
+

Guardie Disponibili

+ {guards && guards.length > 0 ? ( +
+ {guards.map((guard) => { + const assigned = isGuardAssigned(guard.id); + const canAssign = canGuardBeAssigned(guard); + + return ( +
+
+
+

+ {guard.user?.firstName} {guard.user?.lastName} +

+ + #{guard.badgeNumber} + +
+
+ {guard.isArmed && ( + + + Armato + + )} + {guard.hasDriverLicense && ( + + + Patente + + )} + {guard.hasFirstAid && ( + + + Primo Soccorso + + )} + {guard.hasFireSafety && ( + + + Antincendio + + )} +
+
+ +
+ ); + })} +
+ ) : ( +

Nessuna guardia disponibile

+ )} +
+
+ )} +
+
); } diff --git a/server/routes.ts b/server/routes.ts index 5083cbe..850ba1b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -247,6 +247,16 @@ export async function registerRoutes(app: Express): Promise { } }); + app.delete("/api/shift-assignments/:id", isAuthenticated, async (req, res) => { + try { + await storage.deleteShiftAssignment(req.params.id); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting shift assignment:", error); + res.status(500).json({ message: "Failed to delete shift assignment" }); + } + }); + // ============= NOTIFICATION ROUTES ============= app.get("/api/notifications", isAuthenticated, async (req: any, res) => { try { diff --git a/server/storage.ts b/server/storage.ts index cfe4383..6250dc9 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -207,6 +207,12 @@ export class DatabaseStorage implements IStorage { return newAssignment; } + async deleteShiftAssignment(id: string): Promise { + await db + .delete(shiftAssignments) + .where(eq(shiftAssignments.id, id)); + } + // Notification operations async getNotificationsByUser(userId: string): Promise { return await db