Implement a new feature for operational planning that allows users to select sites, assign guards and vehicles, and create shifts with specific start and end times. This includes updates to the UI for displaying uncovered sites and resources, as well as backend logic for creating shift assignments. 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/sshIJbn
1425 lines
51 KiB
TypeScript
1425 lines
51 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" });
|
|
}
|
|
|
|
// Convert and validate dates
|
|
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" });
|
|
}
|
|
|
|
// 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;
|
|
}
|