diff --git a/.replit b/.replit index 269b307..c50bc15 100644 --- a/.replit +++ b/.replit @@ -19,10 +19,6 @@ externalPort = 80 localPort = 33035 externalPort = 3001 -[[ports]] -localPort = 38383 -externalPort = 4200 - [[ports]] localPort = 41343 externalPort = 3000 diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx index db0b198..e7283b6 100644 --- a/client/src/pages/general-planning.tsx +++ b/client/src/pages/general-planning.tsx @@ -75,9 +75,10 @@ export default function GeneralPlanning() { const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date(), { weekStartsOn: 1 })); const [selectedCell, setSelectedCell] = useState<{ siteId: string; siteName: string; date: string; data: SiteData } | null>(null); - // Form state per creazione turno + // Form state per assegnazione guardia const [selectedGuardId, setSelectedGuardId] = useState(""); - const [days, setDays] = useState(1); + const [startTime, setStartTime] = useState("06:00"); + const [durationHours, setDurationHours] = useState(8); // Query per dati planning settimanale const { data: planningData, isLoading } = useQuery({ @@ -91,13 +92,30 @@ export default function GeneralPlanning() { }, }); + // Calcola start e end time per la query availability + const getTimeSlot = () => { + if (!selectedCell) return { start: "", end: "" }; + const [hours, minutes] = startTime.split(":").map(Number); + const startDateTime = new Date(selectedCell.date); + startDateTime.setHours(hours, minutes, 0, 0); + + const endDateTime = new Date(startDateTime); + endDateTime.setHours(startDateTime.getHours() + durationHours); + + return { + start: startDateTime.toISOString(), + end: endDateTime.toISOString() + }; + }; + // Query per guardie disponibili (solo quando dialog è aperto) const { data: availableGuards, isLoading: isLoadingGuards } = useQuery({ - queryKey: ["/api/guards/availability", format(weekStart, "yyyy-MM-dd"), selectedCell?.siteId, selectedLocation], + queryKey: ["/api/guards/availability", selectedCell?.siteId, selectedLocation, startTime, durationHours], queryFn: async () => { if (!selectedCell) return []; + const { start, end } = getTimeSlot(); const response = await fetch( - `/api/guards/availability?weekStart=${format(weekStart, "yyyy-MM-dd")}&siteId=${selectedCell.siteId}&location=${selectedLocation}` + `/api/guards/availability?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}&siteId=${selectedCell.siteId}&location=${selectedLocation}` ); if (!response.ok) throw new Error("Failed to fetch guards availability"); return response.json(); @@ -105,10 +123,10 @@ export default function GeneralPlanning() { enabled: !!selectedCell, // Query attiva solo se dialog è aperto }); - // Mutation per creare turno multi-giorno - const createShiftMutation = useMutation({ - mutationFn: async (data: { siteId: string; startDate: string; days: number; guardId: string }) => { - return apiRequest("POST", "/api/general-planning/shifts", data); + // Mutation per assegnare guardia con orari + const assignGuardMutation = useMutation({ + mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number }) => { + return apiRequest("POST", "/api/general-planning/assign-guard", data); }, onSuccess: () => { // Invalida cache planning generale @@ -116,33 +134,56 @@ export default function GeneralPlanning() { queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] }); toast({ - title: "Turno creato", - description: "Il turno è stato creato con successo", + title: "Guardia assegnata", + description: "La guardia è stata assegnata con successo", }); - // Reset form e chiudi dialog + // Reset form setSelectedGuardId(""); - setDays(1); setSelectedCell(null); }, onError: (error: any) => { toast({ title: "Errore", - description: error.message || "Impossibile creare il turno", + description: error.message || "Impossibile assegnare la guardia", variant: "destructive", }); }, }); - // Handler per submit form creazione turno - const handleCreateShift = () => { + // Mutation per deassegnare guardia + const unassignGuardMutation = useMutation({ + mutationFn: async (assignmentId: string) => { + return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, {}); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] }); + queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] }); + + toast({ + title: "Guardia deassegnata", + description: "La guardia è stata rimossa dal turno", + }); + }, + onError: (error: any) => { + toast({ + title: "Errore", + description: error.message || "Impossibile deassegnare la guardia", + variant: "destructive", + }); + }, + }); + + // Handler per submit form assegnazione guardia + const handleAssignGuard = () => { if (!selectedCell || !selectedGuardId) return; - createShiftMutation.mutate({ + assignGuardMutation.mutate({ siteId: selectedCell.siteId, - startDate: selectedCell.date, - days, + date: selectedCell.date, guardId: selectedGuardId, + startTime, + durationHours, }); }; @@ -525,14 +566,51 @@ export default function GeneralPlanning() { )} - {/* Form creazione nuovo turno */} + {/* Form assegnazione guardia */}
- Crea Nuovo Turno + Assegna Guardia
+ {/* Ora Inizio e Durata */} +
+
+ + setStartTime(e.target.value)} + disabled={assignGuardMutation.isPending} + data-testid="input-start-time" + /> +
+
+ + setDurationHours(Math.max(1, Math.min(24, parseInt(e.target.value) || 8)))} + disabled={assignGuardMutation.isPending} + data-testid="input-duration" + /> +
+
+ + {/* Ora fine calcolata */} +
+ Ora fine: {(() => { + const [hours, minutes] = startTime.split(":").map(Number); + const endHour = (hours + durationHours) % 24; + return `${String(endHour).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; + })()} +
+ {/* Select guardia disponibile */}
@@ -542,7 +620,7 @@ export default function GeneralPlanning() { )} {availableGuards && availableGuards.length > 0 && selectedGuardId && ( -

+

{(() => { const guard = availableGuards.find(g => g.guardId === selectedGuardId); - return guard ? `Ore assegnate: ${guard.weeklyHoursAssigned}h / ${guard.weeklyHoursMax}h (rimangono ${guard.weeklyHoursRemaining}h)` : ""; + if (!guard) return null; + return ( + <> +

+ Ore assegnate: {guard.weeklyHoursAssigned}h / {guard.weeklyHoursMax}h (rimangono {guard.weeklyHoursRemaining}h) +

+ {guard.conflicts && guard.conflicts.length > 0 && ( +

+ ⚠️ Conflitto: {guard.conflicts.join(", ")} +

+ )} + {guard.unavailabilityReasons && guard.unavailabilityReasons.length > 0 && ( +

+ {guard.unavailabilityReasons.join(", ")} +

+ )} + + ); })()} -

+
)}
- {/* Input numero giorni */} -
- - setDays(Math.max(1, Math.min(7, parseInt(e.target.value) || 1)))} - disabled={createShiftMutation.isPending} - data-testid="input-days" - /> -

- Il turno verrà creato a partire da {selectedCell && format(new Date(selectedCell.date), "dd/MM/yyyy")} per {days} {days === 1 ? "giorno" : "giorni"} -

-
- - {/* Bottone crea turno */} + {/* Bottone assegna */} diff --git a/server/routes.ts b/server/routes.ts index f288d0b..4cdde8d 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1177,6 +1177,125 @@ export async function registerRoutes(app: Express): Promise { } }); + // Assign guard to site/date with specific time slot + app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => { + try { + const { siteId, date, guardId, startTime, durationHours } = req.body; + + if (!siteId || !date || !guardId || !startTime || !durationHours) { + return res.status(400).json({ + message: "Missing required fields: siteId, date, guardId, startTime, durationHours" + }); + } + + // Get site to check contract and service details + const site = await storage.getSite(siteId); + if (!site) { + return res.status(404).json({ message: "Site not found" }); + } + + // Get guard to verify it exists + const guard = await storage.getGuard(guardId); + if (!guard) { + return res.status(404).json({ message: "Guard not found" }); + } + + // Parse date and time + const shiftDate = parseISO(date); + if (!isValid(shiftDate)) { + return res.status(400).json({ message: "Invalid date format" }); + } + + // Calculate planned start and end times + const [hours, minutes] = startTime.split(":").map(Number); + const plannedStart = new Date(shiftDate); + plannedStart.setHours(hours, minutes, 0, 0); + + const plannedEnd = new Date(plannedStart); + plannedEnd.setHours(plannedStart.getHours() + durationHours); + + // Check contract validity + if (site.contractStartDate && site.contractEndDate) { + const contractStart = new Date(site.contractStartDate); + const contractEnd = new Date(site.contractEndDate); + if (shiftDate < contractStart || shiftDate > contractEnd) { + return res.status(400).json({ + message: `Cannot assign guard: date outside contract period` + }); + } + } + + // Find or create shift for this site/date (spanning the full service period) + const dayStart = new Date(shiftDate); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(shiftDate); + dayEnd.setHours(23, 59, 59, 999); + + let existingShifts = await db + .select() + .from(shifts) + .where( + and( + eq(shifts.siteId, siteId), + gte(shifts.startTime, dayStart), + lte(shifts.startTime, dayEnd) + ) + ); + + let shift; + if (existingShifts.length > 0) { + // Use existing shift + shift = existingShifts[0]; + } else { + // Create new shift for full service period + const serviceStart = site.serviceStartTime || "00:00"; + const serviceEnd = site.serviceEndTime || "23:59"; + + const [startHour, startMin] = serviceStart.split(":").map(Number); + const [endHour, endMin] = serviceEnd.split(":").map(Number); + + const shiftStart = new Date(shiftDate); + shiftStart.setHours(startHour, startMin, 0, 0); + + const shiftEnd = new Date(shiftDate); + shiftEnd.setHours(endHour, endMin, 0, 0); + + // If service ends before it starts, it spans midnight + if (shiftEnd <= shiftStart) { + shiftEnd.setDate(shiftEnd.getDate() + 1); + } + + [shift] = await db.insert(shifts).values({ + siteId: site.id, + startTime: shiftStart, + endTime: shiftEnd, + shiftType: site.shiftType || "fixed_post", + status: "planned", + }).returning(); + } + + // Create shift assignment with planned time slot + const assignment = await storage.createShiftAssignment({ + shiftId: shift.id, + guardId: guard.id, + plannedStartTime: plannedStart, + plannedEndTime: plannedEnd, + }); + + res.json({ + message: "Guard assigned successfully", + assignment, + shift + }); + } catch (error: any) { + console.error("Error assigning guard:", error); + if (error.message?.includes('overlap') || error.message?.includes('conflict')) { + return res.status(409).json({ message: error.message }); + } + res.status(500).json({ message: "Failed to assign guard", error: String(error) }); + } + }); + // ============= CERTIFICATION ROUTES ============= app.post("/api/certifications", isAuthenticated, async (req, res) => { try {