import type { Express } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage"; import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth"; import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth"; import { db } from "./db"; import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes } from "@shared/schema"; import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm"; import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns"; // Determina quale sistema auth usare basandosi sull'ambiente const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS; // Helper per estrarre user ID in modo compatibile function getUserId(req: any): string { if (USE_LOCAL_AUTH) { return req.user?.id || ""; } else { return req.user?.claims?.sub || ""; } } // Usa il middleware auth appropriato const isAuthenticated = USE_LOCAL_AUTH ? isAuthenticatedLocal : isAuthenticatedReplit; export async function registerRoutes(app: Express): Promise { // Setup auth system appropriato if (USE_LOCAL_AUTH) { console.log("🔐 Usando Local Auth (vt.alfacom.it)"); await setupLocalAuth(app); } else { console.log("🔐 Usando Replit OIDC Auth"); await setupReplitAuth(app); } // ============= AUTH ROUTES ============= app.get("/api/auth/user", isAuthenticated, async (req: any, res) => { try { const userId = getUserId(req); const user = await storage.getUser(userId); res.json(user); } catch (error) { console.error("Error fetching user:", error); res.status(500).json({ message: "Failed to fetch user" }); } }); // ============= USER MANAGEMENT ROUTES ============= app.get("/api/users", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can view all users if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } const allUsers = await storage.getAllUsers(); res.json(allUsers); } catch (error) { console.error("Error fetching users:", error); res.status(500).json({ message: "Failed to fetch users" }); } }); app.post("/api/users", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can create users if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } const { email, firstName, lastName, password, role } = req.body; if (!email || !firstName || !lastName || !password) { return res.status(400).json({ message: "Missing required fields" }); } // Hash password const bcrypt = await import("bcrypt"); const passwordHash = await bcrypt.hash(password, 10); // Generate UUID const crypto = await import("crypto"); const userId = crypto.randomUUID(); const newUser = await storage.upsertUser({ id: userId, email, firstName, lastName, profileImageUrl: null, passwordHash, }); // Set role if provided if (role) { await storage.updateUserRole(newUser.id, role); } res.json(newUser); } catch (error: any) { console.error("Error creating user:", error); if (error.code === '23505') { // Unique violation return res.status(409).json({ message: "Email già esistente" }); } res.status(500).json({ message: "Failed to create user" }); } }); app.patch("/api/users/:id", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can update user roles if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } // Prevent admins from changing their own role if (req.params.id === currentUserId) { return res.status(403).json({ message: "Cannot change your own role" }); } const { role } = req.body; if (!role || !["admin", "coordinator", "guard", "client"].includes(role)) { return res.status(400).json({ message: "Invalid role" }); } const updated = await storage.updateUserRole(req.params.id, role); if (!updated) { return res.status(404).json({ message: "User not found" }); } res.json(updated); } catch (error) { console.error("Error updating user role:", error); res.status(500).json({ message: "Failed to update user role" }); } }); app.put("/api/users/:id", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can update users if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } const { email, firstName, lastName, password, role } = req.body; const existingUser = await storage.getUser(req.params.id); if (!existingUser) { return res.status(404).json({ message: "User not found" }); } // Prepare update data const updateData: any = { id: req.params.id, email: email || existingUser.email, firstName: firstName || existingUser.firstName, lastName: lastName || existingUser.lastName, profileImageUrl: existingUser.profileImageUrl, }; // Hash new password if provided if (password) { const bcrypt = await import("bcrypt"); updateData.passwordHash = await bcrypt.hash(password, 10); } const updated = await storage.upsertUser(updateData); // Update role if provided and not changing own role if (role && req.params.id !== currentUserId) { await storage.updateUserRole(req.params.id, role); } res.json(updated); } catch (error: any) { console.error("Error updating user:", error); if (error.code === '23505') { return res.status(409).json({ message: "Email già esistente" }); } res.status(500).json({ message: "Failed to update user" }); } }); app.delete("/api/users/:id", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can delete users if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } // Prevent admins from deleting themselves if (req.params.id === currentUserId) { return res.status(403).json({ message: "Cannot delete your own account" }); } await storage.deleteUser(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error deleting user:", error); res.status(500).json({ message: "Failed to delete user" }); } }); // ============= GUARD ROUTES ============= app.get("/api/guards", isAuthenticated, async (req, res) => { try { const allGuards = await storage.getAllGuards(); // Fetch related data for each guard const guardsWithDetails = await Promise.all( allGuards.map(async (guard) => { const certs = await storage.getCertificationsByGuard(guard.id); const user = guard.userId ? await storage.getUser(guard.userId) : undefined; // Update certification status based on expiry date for (const cert of certs) { const daysUntilExpiry = differenceInDays(new Date(cert.expiryDate), new Date()); let newStatus: "valid" | "expiring_soon" | "expired" = "valid"; if (daysUntilExpiry < 0) { newStatus = "expired"; } else if (daysUntilExpiry <= 30) { newStatus = "expiring_soon"; } if (cert.status !== newStatus) { await storage.updateCertificationStatus(cert.id, newStatus); cert.status = newStatus; } } return { ...guard, certifications: certs, user, }; }) ); res.json(guardsWithDetails); } catch (error) { console.error("Error fetching guards:", error); res.status(500).json({ message: "Failed to fetch guards" }); } }); app.post("/api/guards", isAuthenticated, async (req, res) => { try { const guard = await storage.createGuard(req.body); res.json(guard); } catch (error) { console.error("Error creating guard:", error); res.status(500).json({ message: "Failed to create guard" }); } }); app.patch("/api/guards/:id", isAuthenticated, async (req, res) => { try { const updated = await storage.updateGuard(req.params.id, req.body); if (!updated) { return res.status(404).json({ message: "Guard not found" }); } res.json(updated); } catch (error) { console.error("Error updating guard:", error); res.status(500).json({ message: "Failed to update guard" }); } }); app.delete("/api/guards/:id", isAuthenticated, async (req, res) => { try { const deleted = await storage.deleteGuard(req.params.id); if (!deleted) { return res.status(404).json({ message: "Guard not found" }); } res.json({ success: true }); } catch (error) { console.error("Error deleting guard:", error); res.status(500).json({ message: "Failed to delete guard" }); } }); // ============= VEHICLE ROUTES ============= app.get("/api/vehicles", isAuthenticated, async (req, res) => { try { const vehicles = await storage.getAllVehicles(); res.json(vehicles); } catch (error) { console.error("Error fetching vehicles:", error); res.status(500).json({ message: "Failed to fetch vehicles" }); } }); app.post("/api/vehicles", isAuthenticated, async (req, res) => { try { const vehicle = await storage.createVehicle(req.body); res.json(vehicle); } catch (error: any) { console.error("Error creating vehicle:", error); if (error.code === '23505') { return res.status(409).json({ message: "Targa già esistente" }); } res.status(500).json({ message: "Failed to create vehicle" }); } }); app.patch("/api/vehicles/:id", isAuthenticated, async (req, res) => { try { const updated = await storage.updateVehicle(req.params.id, req.body); if (!updated) { return res.status(404).json({ message: "Vehicle not found" }); } res.json(updated); } catch (error: any) { console.error("Error updating vehicle:", error); if (error.code === '23505') { return res.status(409).json({ message: "Targa già esistente" }); } res.status(500).json({ message: "Failed to update vehicle" }); } }); app.delete("/api/vehicles/:id", isAuthenticated, async (req, res) => { try { const deleted = await storage.deleteVehicle(req.params.id); if (!deleted) { return res.status(404).json({ message: "Vehicle not found" }); } res.json({ success: true }); } catch (error) { console.error("Error deleting vehicle:", error); res.status(500).json({ message: "Failed to delete vehicle" }); } }); // ============= CONTRACT PARAMETERS ROUTES ============= app.get("/api/contract-parameters", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins and coordinators can view parameters if (currentUser?.role !== "admin" && currentUser?.role !== "coordinator") { return res.status(403).json({ message: "Forbidden: Admin or Coordinator access required" }); } let params = await storage.getContractParameters(); // Se non esistono parametri, creali con valori di default CCNL if (!params) { params = await storage.createContractParameters({ contractType: "CCNL_VIGILANZA_2024", }); } res.json(params); } catch (error) { console.error("Error fetching contract parameters:", error); res.status(500).json({ message: "Failed to fetch contract parameters" }); } }); app.put("/api/contract-parameters/:id", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can update parameters if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } // Validate request body with insert schema const { insertContractParametersSchema } = await import("@shared/schema"); const validationResult = insertContractParametersSchema.partial().safeParse(req.body); if (!validationResult.success) { return res.status(400).json({ message: "Invalid parameters data", errors: validationResult.error.errors }); } const updated = await storage.updateContractParameters(req.params.id, validationResult.data); if (!updated) { return res.status(404).json({ message: "Contract parameters not found" }); } res.json(updated); } catch (error) { console.error("Error updating contract parameters:", error); res.status(500).json({ message: "Failed to update contract parameters" }); } }); // ============= CCNL SETTINGS ROUTES ============= app.get("/api/ccnl-settings", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins and coordinators can view CCNL settings if (currentUser?.role !== "admin" && currentUser?.role !== "coordinator") { return res.status(403).json({ message: "Forbidden: Admin or Coordinator access required" }); } const settings = await storage.getAllCcnlSettings(); res.json(settings); } catch (error) { console.error("Error fetching CCNL settings:", error); res.status(500).json({ message: "Failed to fetch CCNL settings" }); } }); app.get("/api/ccnl-settings/:key", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins and coordinators can view CCNL settings if (currentUser?.role !== "admin" && currentUser?.role !== "coordinator") { return res.status(403).json({ message: "Forbidden: Admin or Coordinator access required" }); } const setting = await storage.getCcnlSetting(req.params.key); if (!setting) { return res.status(404).json({ message: "CCNL setting not found" }); } res.json(setting); } catch (error) { console.error("Error fetching CCNL setting:", error); res.status(500).json({ message: "Failed to fetch CCNL setting" }); } }); app.post("/api/ccnl-settings", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can create/update CCNL settings if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } const { insertCcnlSettingSchema } = await import("@shared/schema"); const validationResult = insertCcnlSettingSchema.safeParse(req.body); if (!validationResult.success) { return res.status(400).json({ message: "Invalid CCNL setting data", errors: validationResult.error.errors }); } const setting = await storage.upsertCcnlSetting(validationResult.data); res.json(setting); } catch (error) { console.error("Error creating/updating CCNL setting:", error); res.status(500).json({ message: "Failed to create/update CCNL setting" }); } }); app.delete("/api/ccnl-settings/:key", isAuthenticated, async (req: any, res) => { try { const currentUserId = getUserId(req); const currentUser = await storage.getUser(currentUserId); // Only admins can delete CCNL settings if (currentUser?.role !== "admin") { return res.status(403).json({ message: "Forbidden: Admin access required" }); } await storage.deleteCcnlSetting(req.params.key); res.json({ success: true }); } catch (error) { console.error("Error deleting CCNL setting:", error); res.status(500).json({ message: "Failed to delete CCNL setting" }); } }); // ============= CCNL VALIDATION ROUTES ============= app.post("/api/ccnl/validate-shift", isAuthenticated, async (req, res) => { try { const { validateShiftForGuard } = await import("./ccnlRules"); const { guardId, shiftStartTime, shiftEndTime, excludeShiftId } = req.body; if (!guardId || !shiftStartTime || !shiftEndTime) { return res.status(400).json({ message: "Missing required fields: guardId, shiftStartTime, shiftEndTime" }); } const result = await validateShiftForGuard( guardId, new Date(shiftStartTime), new Date(shiftEndTime), excludeShiftId ); res.json(result); } catch (error) { console.error("Error validating shift:", error); res.status(500).json({ message: "Failed to validate shift" }); } }); app.get("/api/ccnl/guard-availability/:guardId", isAuthenticated, async (req, res) => { try { const { getGuardAvailabilityReport } = await import("./ccnlRules"); const { guardId } = req.params; const startDate = req.query.startDate ? new Date(req.query.startDate as string) : new Date(); const endDate = req.query.endDate ? new Date(req.query.endDate as string) : new Date(); const report = await getGuardAvailabilityReport(guardId, startDate, endDate); res.json(report); } catch (error) { console.error("Error fetching guard availability:", error); res.status(500).json({ message: "Failed to fetch guard availability" }); } }); // ============= OPERATIONAL PLANNING ROUTES ============= app.get("/api/operational-planning/availability", isAuthenticated, async (req, res) => { try { const { getGuardAvailabilityReport } = await import("./ccnlRules"); // Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17" const rawDateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd"); const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash // Valida la data const parsedDate = parseISO(normalizedDateStr); if (!isValid(parsedDate)) { return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); } const dateStr = format(parsedDate, "yyyy-MM-dd"); // Ottieni location dalla query (default: roccapiemonte) const location = req.query.location as string || "roccapiemonte"; // Imposta inizio e fine giornata in UTC const startOfDay = new Date(dateStr + "T00:00:00.000Z"); const endOfDay = new Date(dateStr + "T23:59:59.999Z"); // Ottieni tutti i veicoli della sede selezionata const allVehicles = await storage.getAllVehicles(); const locationVehicles = allVehicles.filter(v => v.location === location); // Ottieni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location) const dayShifts = await db .select({ shift: shifts }) .from(shifts) .innerJoin(sites, eq(shifts.siteId, sites.id)) .where( and( gte(shifts.startTime, startOfDay), lte(shifts.startTime, endOfDay), eq(sites.location, location) ) ); // Mappa veicoli con disponibilità const vehiclesWithAvailability = await Promise.all( locationVehicles.map(async (vehicle) => { const assignedShiftRecord = dayShifts.find((s: any) => s.shift.vehicleId === vehicle.id); return { ...vehicle, isAvailable: !assignedShiftRecord, assignedShift: assignedShiftRecord ? { id: assignedShiftRecord.shift.id, startTime: assignedShiftRecord.shift.startTime, endTime: assignedShiftRecord.shift.endTime, siteId: assignedShiftRecord.shift.siteId } : null }; }) ); // Ottieni tutte le guardie della sede selezionata const allGuards = await storage.getAllGuards(); const locationGuards = allGuards.filter(g => g.location === location); // Ottieni assegnazioni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location) const dayShiftAssignments = await db .select() .from(shiftAssignments) .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) .innerJoin(sites, eq(shifts.siteId, sites.id)) .where( and( gte(shifts.startTime, startOfDay), lte(shifts.startTime, endOfDay), eq(sites.location, location) ) ); // Calcola disponibilità agenti con report CCNL const guardsWithAvailability = await Promise.all( locationGuards.map(async (guard) => { const assignedShift = dayShiftAssignments.find( (assignment: any) => assignment.shift_assignments.guardId === guard.id ); // Calcola report disponibilità CCNL const availabilityReport = await getGuardAvailabilityReport( guard.id, startOfDay, endOfDay ); return { ...guard, isAvailable: !assignedShift, assignedShift: assignedShift ? { id: assignedShift.shifts.id, startTime: assignedShift.shifts.startTime, endTime: assignedShift.shifts.endTime, siteId: assignedShift.shifts.siteId } : null, availability: { weeklyHours: availabilityReport.weeklyHours.current, remainingWeeklyHours: availabilityReport.remainingWeeklyHours, remainingMonthlyHours: availabilityReport.remainingMonthlyHours, consecutiveDaysWorked: availabilityReport.consecutiveDaysWorked } }; }) ); // Ordina veicoli: disponibili prima, poi per targa const sortedVehicles = vehiclesWithAvailability.sort((a, b) => { if (a.isAvailable && !b.isAvailable) return -1; if (!a.isAvailable && b.isAvailable) return 1; return a.licensePlate.localeCompare(b.licensePlate); }); // Ordina agenti: disponibili prima, poi per ore settimanali (meno ore = più disponibili) const sortedGuards = guardsWithAvailability.sort((a, b) => { if (a.isAvailable && !b.isAvailable) return -1; if (!a.isAvailable && b.isAvailable) return 1; // Se entrambi disponibili, ordina per ore settimanali (meno ore = prima) if (a.isAvailable && b.isAvailable) { return a.availability.weeklyHours - b.availability.weeklyHours; } return 0; }); res.json({ date: dateStr, vehicles: sortedVehicles, guards: sortedGuards }); } catch (error) { console.error("Error fetching operational planning availability:", error); res.status(500).json({ message: "Failed to fetch availability", error: String(error) }); } }); // Endpoint per ottenere siti non completamente coperti per una data e sede app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => { try { // Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17" const rawDateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd"); const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash // Valida la data const parsedDate = parseISO(normalizedDateStr); if (!isValid(parsedDate)) { return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); } const dateStr = format(parsedDate, "yyyy-MM-dd"); // Ottieni location dalla query (default: roccapiemonte) const location = req.query.location as string || "roccapiemonte"; // Ottieni tutti i siti attivi della sede selezionata const allSites = await db .select() .from(sites) .where( and( eq(sites.isActive, true), eq(sites.location, location) ) ); // Filtra siti con contratto valido per la data selezionata const sitesWithValidContract = allSites.filter((site: any) => { // Se il sito non ha date contrattuali, lo escludiamo if (!site.contractStartDate || !site.contractEndDate) { return false; } // Normalizza date per confronto day-only const selectedDate = new Date(dateStr); selectedDate.setHours(0, 0, 0, 0); const contractStart = new Date(site.contractStartDate); contractStart.setHours(0, 0, 0, 0); const contractEnd = new Date(site.contractEndDate); contractEnd.setHours(23, 59, 59, 999); // Verifica che la data selezionata sia dentro il periodo contrattuale return selectedDate >= contractStart && selectedDate <= contractEnd; }); // Ottieni turni del giorno con assegnazioni SOLO della sede selezionata const startOfDayDate = new Date(dateStr); startOfDayDate.setHours(0, 0, 0, 0); const endOfDayDate = new Date(dateStr); endOfDayDate.setHours(23, 59, 59, 999); const dayShifts = await db .select({ shift: shifts, assignmentCount: sql`count(${shiftAssignments.id})::int` }) .from(shifts) .innerJoin(sites, eq(shifts.siteId, sites.id)) .leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId)) .where( and( gte(shifts.startTime, startOfDayDate), lte(shifts.startTime, endOfDayDate), ne(shifts.status, "cancelled"), eq(sites.location, location) ) ) .groupBy(shifts.id); // Calcola copertura per ogni sito con contratto valido const sitesWithCoverage = sitesWithValidContract.map((site: any) => { const siteShifts = dayShifts.filter((s: any) => s.shift.siteId === site.id); // Verifica copertura per ogni turno const shiftsWithCoverage = siteShifts.map((s: any) => ({ id: s.shift.id, startTime: s.shift.startTime, endTime: s.shift.endTime, assignedGuardsCount: s.assignmentCount, requiredGuards: site.minGuards, isCovered: s.assignmentCount >= site.minGuards, isPartial: s.assignmentCount > 0 && s.assignmentCount < site.minGuards })); // Un sito è completamente coperto solo se TUTTI i turni hanno il numero minimo di guardie const allShiftsCovered = siteShifts.length > 0 && shiftsWithCoverage.every((s: any) => s.isCovered); // Un sito è parzialmente coperto se ha turni ma non tutti sono completamente coperti const hasPartialCoverage = siteShifts.length > 0 && !allShiftsCovered && shiftsWithCoverage.some((s: any) => s.assignedGuardsCount > 0); // Calcola totale guardie assegnate per info const totalAssignedGuards = siteShifts.reduce((sum: number, s: any) => sum + s.assignmentCount, 0); return { ...site, isCovered: allShiftsCovered, isPartiallyCovered: hasPartialCoverage, totalAssignedGuards, requiredGuards: site.minGuards, shiftsCount: siteShifts.length, shifts: shiftsWithCoverage }; }); // Filtra solo siti non completamente coperti const uncoveredSites = sitesWithCoverage.filter( (site: any) => !site.isCovered ); // Ordina: parzialmente coperti prima, poi non coperti const sortedUncoveredSites = uncoveredSites.sort((a: any, b: any) => { if (a.isPartiallyCovered && !b.isPartiallyCovered) return -1; if (!a.isPartiallyCovered && b.isPartiallyCovered) return 1; return a.name.localeCompare(b.name); }); res.json({ date: dateStr, uncoveredSites: sortedUncoveredSites, totalSites: sitesWithValidContract.length, totalUncovered: uncoveredSites.length }); } catch (error) { console.error("Error fetching uncovered sites:", error); res.status(500).json({ message: "Failed to fetch uncovered sites", error: String(error) }); } }); // Endpoint per Planning Generale - vista settimanale con calcolo guardie mancanti app.get("/api/general-planning", isAuthenticated, async (req, res) => { try { // Sanitizza input data inizio settimana const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd"); const normalizedWeekStart = rawWeekStart.split("/")[0]; // Valida la data const parsedWeekStart = parseISO(normalizedWeekStart); if (!isValid(parsedWeekStart)) { return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); } const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd"); // Ottieni location dalla query (default: roccapiemonte) const location = req.query.location as string || "roccapiemonte"; // Calcola fine settimana (weekStart + 6 giorni) const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd"); // Ottieni tutti i siti attivi della sede const activeSites = await db .select() .from(sites) .leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id)) .where( and( eq(sites.isActive, true), eq(sites.location, location as any) ) ); // Ottieni tutti i turni della settimana per la sede const weekStartTimestamp = new Date(weekStartDate); weekStartTimestamp.setHours(0, 0, 0, 0); const weekEndTimestamp = new Date(weekEndDate); weekEndTimestamp.setHours(23, 59, 59, 999); const weekShifts = await db .select({ shift: shifts, site: sites, }) .from(shifts) .innerJoin(sites, eq(shifts.siteId, sites.id)) .where( and( gte(shifts.startTime, weekStartTimestamp), lte(shifts.startTime, weekEndTimestamp), ne(shifts.status, "cancelled"), eq(sites.location, location as any) ) ); // Ottieni tutte le assegnazioni dei turni della settimana const shiftIds = weekShifts.map(s => s.shift.id); const assignments = shiftIds.length > 0 ? await db .select({ assignment: shiftAssignments, guard: guards, shift: shifts, }) .from(shiftAssignments) .innerJoin(guards, eq(shiftAssignments.guardId, guards.id)) .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) .where( sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map(id => sql`${id}`), sql`, `)})` ) : []; // Ottieni veicoli assegnati const vehicleAssignments = weekShifts .filter(s => s.shift.vehicleId) .map(s => s.shift.vehicleId); const assignedVehicles = vehicleAssignments.length > 0 ? await db .select() .from(vehicles) .where( sql`${vehicles.id} IN (${sql.join(vehicleAssignments.map(id => sql`${id}`), sql`, `)})` ) : []; // Costruisci struttura dati per 7 giorni const weekData = []; for (let dayOffset = 0; dayOffset < 7; dayOffset++) { const currentDay = addDays(parsedWeekStart, dayOffset); const dayStr = format(currentDay, "yyyy-MM-dd"); const dayStartTimestamp = new Date(dayStr); dayStartTimestamp.setHours(0, 0, 0, 0); const dayEndTimestamp = new Date(dayStr); dayEndTimestamp.setHours(23, 59, 59, 999); const sitesData = activeSites.map(({ sites: site, service_types: serviceType }) => { // Trova turni del giorno per questo sito const dayShifts = weekShifts.filter(s => s.shift.siteId === site.id && s.shift.startTime >= dayStartTimestamp && s.shift.startTime <= dayEndTimestamp ); // Ottieni assegnazioni guardie per i turni del giorno const dayAssignments = assignments.filter(a => dayShifts.some(ds => ds.shift.id === a.shift.id) ); // Calcola ore per ogni guardia const guardsWithHours = dayAssignments.map(a => { const shiftStart = new Date(a.shift.startTime); const shiftEnd = new Date(a.shift.endTime); const hours = differenceInHours(shiftEnd, shiftStart); return { guardId: a.guard.id, guardName: a.guard.fullName, badgeNumber: a.guard.badgeNumber, hours, }; }); // Veicoli assegnati ai turni del giorno const dayVehicles = dayShifts .filter(ds => ds.shift.vehicleId) .map(ds => { const vehicle = assignedVehicles.find(v => v.id === ds.shift.vehicleId); return vehicle ? { vehicleId: vehicle.id, licensePlate: vehicle.licensePlate, brand: vehicle.brand, model: vehicle.model, } : null; }) .filter(Boolean); // Calcolo guardie mancanti // Formula: ceil(24 / maxOreGuardia) × minGuardie - guardieAssegnate const maxOreGuardia = 9; // Max ore per guardia const minGuardie = site.minGuards || 1; // Somma ore totali dei turni del giorno const totalShiftHours = dayShifts.reduce((sum, ds) => { const start = new Date(ds.shift.startTime); const end = new Date(ds.shift.endTime); return sum + differenceInHours(end, start); }, 0); // Slot necessari per coprire le ore totali const slotsNeeded = totalShiftHours > 0 ? Math.ceil(totalShiftHours / maxOreGuardia) : 0; // Guardie totali necessarie (slot × min guardie contemporanee) const totalGuardsNeeded = slotsNeeded * minGuardie; // Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni) const uniqueGuardsAssigned = new Set(guardsWithHours.map(g => g.guardId)).size; // Guardie mancanti const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned); return { siteId: site.id, siteName: site.name, serviceType: serviceType?.label || "N/A", minGuards: site.minGuards, guards: guardsWithHours, vehicles: dayVehicles, totalShiftHours, guardsAssigned: uniqueGuardsAssigned, missingGuards, shiftsCount: dayShifts.length, }; }); weekData.push({ date: dayStr, dayOfWeek: format(currentDay, "EEEE"), sites: sitesData, }); } res.json({ weekStart: weekStartDate, weekEnd: weekEndDate, location, days: weekData, }); } catch (error) { console.error("Error fetching general planning:", error); res.status(500).json({ message: "Failed to fetch general planning", error: String(error) }); } }); // ============= CERTIFICATION ROUTES ============= app.post("/api/certifications", isAuthenticated, async (req, res) => { try { const cert = await storage.createCertification(req.body); res.json(cert); } catch (error) { console.error("Error creating certification:", error); res.status(500).json({ message: "Failed to create certification" }); } }); // ============= SERVICE TYPE ROUTES ============= app.get("/api/service-types", isAuthenticated, async (req, res) => { try { const serviceTypes = await storage.getAllServiceTypes(); res.json(serviceTypes); } catch (error) { console.error("Error fetching service types:", error); res.status(500).json({ message: "Failed to fetch service types" }); } }); app.post("/api/service-types", isAuthenticated, async (req, res) => { try { const serviceType = await storage.createServiceType(req.body); res.json(serviceType); } catch (error) { console.error("Error creating service type:", error); res.status(500).json({ message: "Failed to create service type" }); } }); app.patch("/api/service-types/:id", isAuthenticated, async (req, res) => { try { const serviceType = await storage.updateServiceType(req.params.id, req.body); if (!serviceType) { return res.status(404).json({ message: "Service type not found" }); } res.json(serviceType); } catch (error) { console.error("Error updating service type:", error); res.status(500).json({ message: "Failed to update service type" }); } }); app.delete("/api/service-types/:id", isAuthenticated, async (req, res) => { try { const serviceType = await storage.deleteServiceType(req.params.id); if (!serviceType) { return res.status(404).json({ message: "Service type not found" }); } res.json(serviceType); } catch (error) { console.error("Error deleting service type:", error); res.status(500).json({ message: "Failed to delete service type" }); } }); // ============= SITE ROUTES ============= app.get("/api/sites", isAuthenticated, async (req, res) => { try { const allSites = await storage.getAllSites(); res.json(allSites); } catch (error) { console.error("Error fetching sites:", error); res.status(500).json({ message: "Failed to fetch sites" }); } }); app.post("/api/sites", isAuthenticated, async (req, res) => { try { const site = await storage.createSite(req.body); res.json(site); } catch (error) { console.error("Error creating site:", error); res.status(500).json({ message: "Failed to create site" }); } }); app.patch("/api/sites/:id", isAuthenticated, async (req, res) => { try { const updated = await storage.updateSite(req.params.id, req.body); if (!updated) { return res.status(404).json({ message: "Site not found" }); } res.json(updated); } catch (error) { console.error("Error updating site:", error); res.status(500).json({ message: "Failed to update site" }); } }); app.delete("/api/sites/:id", isAuthenticated, async (req, res) => { try { const deleted = await storage.deleteSite(req.params.id); if (!deleted) { return res.status(404).json({ message: "Site not found" }); } res.json({ success: true }); } catch (error) { console.error("Error deleting site:", error); res.status(500).json({ message: "Failed to delete site" }); } }); // ============= SHIFT ROUTES ============= app.get("/api/shifts", isAuthenticated, async (req, res) => { try { const allShifts = await storage.getAllShifts(); // Fetch related data for each shift const shiftsWithDetails = await Promise.all( allShifts.map(async (shift) => { const site = await storage.getSite(shift.siteId); const assignments = await storage.getShiftAssignments(shift.id); // Fetch guard details for each assignment const assignmentsWithGuards = await Promise.all( assignments.map(async (assignment) => { const guard = await storage.getGuard(assignment.guardId); const certs = guard ? await storage.getCertificationsByGuard(guard.id) : []; const user = guard?.userId ? await storage.getUser(guard.userId) : undefined; return { ...assignment, guard: guard ? { ...guard, certifications: certs, user, } : null, }; }) ); return { ...shift, site: site!, assignments: assignmentsWithGuards.filter(a => a.guard !== null), }; }) ); res.json(shiftsWithDetails); } catch (error) { console.error("Error fetching shifts:", error); res.status(500).json({ message: "Failed to fetch shifts" }); } }); app.get("/api/shifts/active", isAuthenticated, async (req, res) => { try { const activeShifts = await storage.getActiveShifts(); // Fetch related data for each shift const shiftsWithDetails = await Promise.all( activeShifts.map(async (shift) => { const site = await storage.getSite(shift.siteId); const assignments = await storage.getShiftAssignments(shift.id); // Fetch guard details for each assignment const assignmentsWithGuards = await Promise.all( assignments.map(async (assignment) => { const guard = await storage.getGuard(assignment.guardId); const certs = guard ? await storage.getCertificationsByGuard(guard.id) : []; const user = guard?.userId ? await storage.getUser(guard.userId) : undefined; return { ...assignment, guard: guard ? { ...guard, certifications: certs, user, } : null, }; }) ); return { ...shift, site: site!, assignments: assignmentsWithGuards.filter(a => a.guard !== null), }; }) ); res.json(shiftsWithDetails); } catch (error) { console.error("Error fetching active shifts:", error); res.status(500).json({ message: "Failed to fetch active shifts" }); } }); app.post("/api/shifts", isAuthenticated, async (req, res) => { try { // Validate that required fields are present and dates are valid strings if (!req.body.siteId || !req.body.startTime || !req.body.endTime) { return res.status(400).json({ message: "Missing required fields" }); } // Verifica stato contratto del sito const site = await storage.getSite(req.body.siteId); if (!site) { return res.status(404).json({ message: "Sito non trovato" }); } // Controllo validità contratto - richiesto per creare turni if (!site.contractStartDate || !site.contractEndDate) { return res.status(400).json({ message: `Impossibile creare turno: il sito "${site.name}" non ha un contratto attivo` }); } // Convert and validate shift dates first const startTime = new Date(req.body.startTime); const endTime = new Date(req.body.endTime); if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) { return res.status(400).json({ message: "Invalid date format" }); } // Normalizza date contratto a giorno intero (00:00 - 23:59) const contractStart = new Date(site.contractStartDate); contractStart.setHours(0, 0, 0, 0); const contractEnd = new Date(site.contractEndDate); contractEnd.setHours(23, 59, 59, 999); // Normalizza data turno a giorno (per confronto) const shiftDate = new Date(startTime); shiftDate.setHours(0, 0, 0, 0); // Verifica che il turno sia dentro il periodo contrattuale if (shiftDate > contractEnd) { return res.status(400).json({ message: `Impossibile creare turno: il contratto per il sito "${site.name}" scade il ${new Date(site.contractEndDate).toLocaleDateString('it-IT')}` }); } if (shiftDate < contractStart) { return res.status(400).json({ message: `Impossibile creare turno: il contratto per il sito "${site.name}" inizia il ${new Date(site.contractStartDate).toLocaleDateString('it-IT')}` }); } // Validate and transform the request body const validatedData = insertShiftSchema.parse({ siteId: req.body.siteId, startTime, endTime, status: req.body.status || "planned", vehicleId: req.body.vehicleId || null, }); const shift = await storage.createShift(validatedData); // Se ci sono guardie da assegnare, crea le assegnazioni if (req.body.guardIds && Array.isArray(req.body.guardIds) && req.body.guardIds.length > 0) { for (const guardId of req.body.guardIds) { await storage.createShiftAssignment({ shiftId: shift.id, guardId, }); } } res.json(shift); } catch (error) { console.error("Error creating shift:", error); res.status(500).json({ message: "Failed to create shift" }); } }); app.patch("/api/shifts/:id/status", isAuthenticated, async (req, res) => { try { await storage.updateShiftStatus(req.params.id, req.body.status); res.json({ success: true }); } catch (error) { console.error("Error updating shift status:", error); res.status(500).json({ message: "Failed to update shift status" }); } }); app.patch("/api/shifts/:id", isAuthenticated, async (req, res) => { try { const { startTime: startTimeStr, endTime: endTimeStr, ...rest } = req.body; const updateData: any = { ...rest }; if (startTimeStr) { const startTime = new Date(startTimeStr); if (isNaN(startTime.getTime())) { return res.status(400).json({ message: "Invalid start time format" }); } updateData.startTime = startTime; } if (endTimeStr) { const endTime = new Date(endTimeStr); if (isNaN(endTime.getTime())) { return res.status(400).json({ message: "Invalid end time format" }); } updateData.endTime = endTime; } const updated = await storage.updateShift(req.params.id, updateData); if (!updated) { return res.status(404).json({ message: "Shift not found" }); } res.json(updated); } catch (error) { console.error("Error updating shift:", error); res.status(500).json({ message: "Failed to update shift" }); } }); app.delete("/api/shifts/:id", isAuthenticated, async (req, res) => { try { const deleted = await storage.deleteShift(req.params.id); if (!deleted) { return res.status(404).json({ message: "Shift not found" }); } res.json({ success: true }); } catch (error) { console.error("Error deleting shift:", error); res.status(500).json({ message: "Failed to delete shift" }); } }); // ============= CCNL VALIDATION ============= app.post("/api/shifts/validate-ccnl", isAuthenticated, async (req, res) => { try { const { guardId, shiftStartTime, shiftEndTime } = req.body; if (!guardId || !shiftStartTime || !shiftEndTime) { return res.status(400).json({ message: "Missing required fields" }); } const startTime = new Date(shiftStartTime); const endTime = new Date(shiftEndTime); // Load CCNL parameters const params = await db.select().from(contractParameters).limit(1); if (params.length === 0) { return res.status(500).json({ message: "CCNL parameters not found" }); } const ccnl = params[0]; const violations: Array<{type: string, message: string}> = []; // Calculate shift hours precisely (in minutes, then convert) const shiftMinutes = differenceInMinutes(endTime, startTime); const shiftHours = Math.round((shiftMinutes / 60) * 100) / 100; // Round to 2 decimals // Check max daily hours if (shiftHours > ccnl.maxHoursPerDay) { violations.push({ type: "MAX_DAILY_HOURS", message: `Turno di ${shiftHours}h supera il limite giornaliero di ${ccnl.maxHoursPerDay}h` }); } // Get guard's shifts for the week (overlapping with week window) const weekStart = startOfWeek(startTime, { weekStartsOn: 1 }); // Monday const weekEnd = endOfWeek(startTime, { weekStartsOn: 1 }); const weekShifts = await db .select() .from(shiftAssignments) .innerJoin(shifts, eq(shifts.id, shiftAssignments.shiftId)) .where( and( eq(shiftAssignments.guardId, guardId), // Shift overlaps week if: (shift_start <= week_end) AND (shift_end >= week_start) lte(shifts.startTime, weekEnd), gte(shifts.endTime, weekStart) ) ); // Calculate weekly hours precisely (only overlap with week window) let weeklyMinutes = 0; // Add overlap of new shift with week (clamp to 0 if outside window) const newShiftStart = startTime < weekStart ? weekStart : startTime; const newShiftEnd = endTime > weekEnd ? weekEnd : endTime; weeklyMinutes += Math.max(0, differenceInMinutes(newShiftEnd, newShiftStart)); // Add overlap of existing shifts with week (clamp to 0 if outside window) for (const { shifts: shift } of weekShifts) { const shiftStart = shift.startTime < weekStart ? weekStart : shift.startTime; const shiftEnd = shift.endTime > weekEnd ? weekEnd : shift.endTime; weeklyMinutes += Math.max(0, differenceInMinutes(shiftEnd, shiftStart)); } const weeklyHours = Math.round((weeklyMinutes / 60) * 100) / 100; if (weeklyHours > ccnl.maxHoursPerWeek) { violations.push({ type: "MAX_WEEKLY_HOURS", message: `Ore settimanali (${weeklyHours}h) superano il limite di ${ccnl.maxHoursPerWeek}h` }); } // Check consecutive days (chronological order ASC, only past shifts before this one) const pastShifts = await db .select() .from(shiftAssignments) .innerJoin(shifts, eq(shifts.id, shiftAssignments.shiftId)) .where( and( eq(shiftAssignments.guardId, guardId), lte(shifts.startTime, startTime) ) ) .orderBy(asc(shifts.startTime)); // Build consecutive days map const shiftDays = new Set(); for (const { shifts: shift } of pastShifts) { shiftDays.add(startOfDay(shift.startTime).toISOString()); } shiftDays.add(startOfDay(startTime).toISOString()); // Count consecutive days backwards from new shift let consecutiveDays = 0; let checkDate = startOfDay(startTime); while (shiftDays.has(checkDate.toISOString())) { consecutiveDays++; checkDate = new Date(checkDate); checkDate.setDate(checkDate.getDate() - 1); } if (consecutiveDays > 6) { violations.push({ type: "MAX_CONSECUTIVE_DAYS", message: `${consecutiveDays} giorni consecutivi superano il limite di 6 giorni` }); } // Check for overlapping shifts (guard already assigned to another shift at same time) // Overlap if: (existing_start < new_end) AND (existing_end > new_start) // Note: Use strict inequalities to allow back-to-back shifts (end == start is OK) const allAssignments = await db .select() .from(shiftAssignments) .innerJoin(shifts, eq(shifts.id, shiftAssignments.shiftId)) .where(eq(shiftAssignments.guardId, guardId)); const overlappingShifts = allAssignments.filter(({ shifts: shift }: any) => { return shift.startTime < endTime && shift.endTime > startTime; }); if (overlappingShifts.length > 0) { violations.push({ type: "OVERLAPPING_SHIFT", message: `Guardia già assegnata a un turno sovrapposto in questo orario` }); } // Check rest hours (only last shift that ended before this one) const lastCompletedShifts = await db .select() .from(shiftAssignments) .innerJoin(shifts, eq(shifts.id, shiftAssignments.shiftId)) .where( and( eq(shiftAssignments.guardId, guardId), lte(shifts.endTime, startTime) ) ) .orderBy(desc(shifts.endTime)) .limit(1); if (lastCompletedShifts.length > 0) { const lastShift = lastCompletedShifts[0].shifts; const restMinutes = differenceInMinutes(startTime, lastShift.endTime); const restHours = Math.round((restMinutes / 60) * 100) / 100; if (restHours < ccnl.minDailyRestHours) { violations.push({ type: "MIN_REST_HOURS", message: `Riposo di ${restHours}h inferiore al minimo di ${ccnl.minDailyRestHours}h` }); } } res.json({ violations, ccnlParams: ccnl }); } catch (error) { console.error("Error validating CCNL:", error); res.status(500).json({ message: "Failed to validate CCNL" }); } }); // ============= SHIFT ASSIGNMENT ROUTES ============= app.post("/api/shift-assignments", isAuthenticated, async (req, res) => { try { const assignment = await storage.createShiftAssignment(req.body); res.json(assignment); } catch (error) { console.error("Error creating shift assignment:", error); res.status(500).json({ message: "Failed to create shift assignment" }); } }); 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 { const userId = getUserId(req); const userNotifications = await storage.getNotificationsByUser(userId); res.json(userNotifications); } catch (error) { console.error("Error fetching user notifications:", error); res.status(500).json({ message: "Failed to fetch notifications" }); } }); app.patch("/api/notifications/:id/read", isAuthenticated, async (req, res) => { try { await storage.markNotificationAsRead(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error marking notification as read:", error); res.status(500).json({ message: "Failed to mark notification as read" }); } }); // ============= GUARD CONSTRAINTS ROUTES ============= app.get("/api/guard-constraints/:guardId", isAuthenticated, async (req, res) => { try { const constraints = await storage.getGuardConstraints(req.params.guardId); res.json(constraints || null); } catch (error) { console.error("Error fetching guard constraints:", error); res.status(500).json({ message: "Failed to fetch guard constraints" }); } }); app.post("/api/guard-constraints", isAuthenticated, async (req, res) => { try { const constraints = await storage.upsertGuardConstraints(req.body); res.json(constraints); } catch (error) { console.error("Error upserting guard constraints:", error); res.status(500).json({ message: "Failed to save guard constraints" }); } }); // ============= SITE PREFERENCES ROUTES ============= app.get("/api/site-preferences/:siteId", isAuthenticated, async (req, res) => { try { const preferences = await storage.getSitePreferences(req.params.siteId); res.json(preferences); } catch (error) { console.error("Error fetching site preferences:", error); res.status(500).json({ message: "Failed to fetch site preferences" }); } }); app.post("/api/site-preferences", isAuthenticated, async (req, res) => { try { const preference = await storage.createSitePreference(req.body); res.json(preference); } catch (error) { console.error("Error creating site preference:", error); res.status(500).json({ message: "Failed to create site preference" }); } }); app.delete("/api/site-preferences/:id", isAuthenticated, async (req, res) => { try { await storage.deleteSitePreference(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error deleting site preference:", error); res.status(500).json({ message: "Failed to delete site preference" }); } }); // ============= TRAINING COURSES ROUTES ============= app.get("/api/training-courses", isAuthenticated, async (req, res) => { try { const guardId = req.query.guardId as string | undefined; const courses = guardId ? await storage.getTrainingCoursesByGuard(guardId) : await storage.getAllTrainingCourses(); res.json(courses); } catch (error) { console.error("Error fetching training courses:", error); res.status(500).json({ message: "Failed to fetch training courses" }); } }); app.post("/api/training-courses", isAuthenticated, async (req, res) => { try { const course = await storage.createTrainingCourse(req.body); res.json(course); } catch (error) { console.error("Error creating training course:", error); res.status(500).json({ message: "Failed to create training course" }); } }); app.patch("/api/training-courses/:id", isAuthenticated, async (req, res) => { try { const updated = await storage.updateTrainingCourse(req.params.id, req.body); if (!updated) { return res.status(404).json({ message: "Training course not found" }); } res.json(updated); } catch (error) { console.error("Error updating training course:", error); res.status(500).json({ message: "Failed to update training course" }); } }); app.delete("/api/training-courses/:id", isAuthenticated, async (req, res) => { try { await storage.deleteTrainingCourse(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error deleting training course:", error); res.status(500).json({ message: "Failed to delete training course" }); } }); // ============= HOLIDAYS ROUTES ============= app.get("/api/holidays", isAuthenticated, async (req, res) => { try { const year = req.query.year ? parseInt(req.query.year as string) : undefined; const holidays = await storage.getAllHolidays(year); res.json(holidays); } catch (error) { console.error("Error fetching holidays:", error); res.status(500).json({ message: "Failed to fetch holidays" }); } }); app.post("/api/holidays", isAuthenticated, async (req, res) => { try { const holiday = await storage.createHoliday(req.body); res.json(holiday); } catch (error) { console.error("Error creating holiday:", error); res.status(500).json({ message: "Failed to create holiday" }); } }); app.delete("/api/holidays/:id", isAuthenticated, async (req, res) => { try { await storage.deleteHoliday(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error deleting holiday:", error); res.status(500).json({ message: "Failed to delete holiday" }); } }); // ============= ABSENCES ROUTES ============= app.get("/api/absences", isAuthenticated, async (req, res) => { try { const guardId = req.query.guardId as string | undefined; const absences = guardId ? await storage.getAbsencesByGuard(guardId) : await storage.getAllAbsences(); res.json(absences); } catch (error) { console.error("Error fetching absences:", error); res.status(500).json({ message: "Failed to fetch absences" }); } }); app.post("/api/absences", isAuthenticated, async (req, res) => { try { const absence = await storage.createAbsence(req.body); res.json(absence); } catch (error) { console.error("Error creating absence:", error); res.status(500).json({ message: "Failed to create absence" }); } }); app.patch("/api/absences/:id", isAuthenticated, async (req, res) => { try { const updated = await storage.updateAbsence(req.params.id, req.body); if (!updated) { return res.status(404).json({ message: "Absence not found" }); } res.json(updated); } catch (error) { console.error("Error updating absence:", error); res.status(500).json({ message: "Failed to update absence" }); } }); app.delete("/api/absences/:id", isAuthenticated, async (req, res) => { try { await storage.deleteAbsence(req.params.id); res.json({ success: true }); } catch (error) { console.error("Error deleting absence:", error); res.status(500).json({ message: "Failed to delete absence" }); } }); const httpServer = createServer(app); return httpServer; }