VigilanzaTurni/server/routes.ts
marco370 4a1c21455b Implement contract rules for shift assignments
Add CCNL (Contratto Collettivo Nazionale di Lavoro) validation logic on the server-side to check shift durations against defined contract parameters. This includes updating the client to handle potential violations and display them to the user via an alert dialog. The server now exposes a new API endpoint (/api/shifts/validate-ccnl) for this validation. Additionally, seeding script has been updated to include default contract parameters.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/IdDfihe
2025-10-17 07:39:19 +00:00

1028 lines
36 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 } from "drizzle-orm";
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO } 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" });
}
});
// ============= 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" });
}
});
// ============= 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",
});
const shift = await storage.createShift(validatedData);
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;
}