VigilanzaTurni/server/routes.ts
marco370 e4d3ab514c Add contract start and end dates for sites and validate shifts
Implement contract start/end date validation for sites and enforce shift creation within contract boundaries on the server. Add contract status display to the client.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/FlO7tHX
2025-10-17 14:35:51 +00:00

1462 lines
52 KiB
TypeScript

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 } 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 } 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<Server> {
// 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");
const dateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
const date = new Date(dateStr + "T00:00:00.000Z");
// 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
const allVehicles = await storage.getAllVehicles();
// Ottieni turni del giorno per trovare veicoli assegnati
const dayShifts = await db
.select()
.from(shifts)
.where(
and(
gte(shifts.startTime, startOfDay),
lte(shifts.startTime, endOfDay)
)
);
// Mappa veicoli con disponibilità
const vehiclesWithAvailability = await Promise.all(
allVehicles.map(async (vehicle) => {
const assignedShift = dayShifts.find((shift: any) => shift.vehicleId === vehicle.id);
return {
...vehicle,
isAvailable: !assignedShift,
assignedShift: assignedShift ? {
id: assignedShift.id,
startTime: assignedShift.startTime,
endTime: assignedShift.endTime,
siteId: assignedShift.siteId
} : null
};
})
);
// Ottieni tutte le guardie
const allGuards = await storage.getAllGuards();
// Ottieni assegnazioni turni del giorno
const dayShiftAssignments = await db
.select()
.from(shiftAssignments)
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
.where(
and(
gte(shifts.startTime, startOfDay),
lte(shifts.startTime, endOfDay)
)
);
// Calcola disponibilità agenti con report CCNL
const guardsWithAvailability = await Promise.all(
allGuards.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
app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => {
try {
const dateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
// 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 siti attivi
const allSites = await db
.select()
.from(sites)
.where(eq(sites.isActive, true));
// Ottieni turni del giorno con assegnazioni
const dayShifts = await db
.select({
shift: shifts,
assignmentCount: sql<number>`count(${shiftAssignments.id})::int`
})
.from(shifts)
.leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId))
.where(
and(
gte(shifts.startTime, startOfDay),
lte(shifts.startTime, endOfDay),
ne(shifts.status, "cancelled")
)
)
.groupBy(shifts.id);
// Calcola copertura per ogni sito
const sitesWithCoverage = allSites.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: allSites.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) });
}
});
// ============= 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<string>();
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;
}