Add general weekly shift planning with missing guard calculations
Introduce a new API endpoint '/api/general-planning' to fetch weekly shift schedules, including active sites, assigned guards, and calculates missing guard requirements based on shift durations and site needs. 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/5GnGQQ0
This commit is contained in:
parent
f0c0321d1a
commit
14758fab56
199
server/routes.ts
199
server/routes.ts
@ -4,9 +4,9 @@ import { storage } from "./storage";
|
|||||||
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
||||||
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters } from "@shared/schema";
|
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes } from "@shared/schema";
|
||||||
import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm";
|
import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm";
|
||||||
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid } from "date-fns";
|
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns";
|
||||||
|
|
||||||
// Determina quale sistema auth usare basandosi sull'ambiente
|
// Determina quale sistema auth usare basandosi sull'ambiente
|
||||||
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
|
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
|
||||||
@ -809,6 +809,201 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Endpoint per Planning Generale - vista settimanale con calcolo guardie mancanti
|
||||||
|
app.get("/api/general-planning", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Sanitizza input data inizio settimana
|
||||||
|
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
|
||||||
|
const normalizedWeekStart = rawWeekStart.split("/")[0];
|
||||||
|
|
||||||
|
// Valida la data
|
||||||
|
const parsedWeekStart = parseISO(normalizedWeekStart);
|
||||||
|
if (!isValid(parsedWeekStart)) {
|
||||||
|
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Ottieni location dalla query (default: roccapiemonte)
|
||||||
|
const location = req.query.location as string || "roccapiemonte";
|
||||||
|
|
||||||
|
// Calcola fine settimana (weekStart + 6 giorni)
|
||||||
|
const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Ottieni tutti i siti attivi della sede
|
||||||
|
const activeSites = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.isActive, true),
|
||||||
|
eq(sites.location, location as any)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ottieni tutti i turni della settimana per la sede
|
||||||
|
const weekStartTimestamp = new Date(weekStartDate);
|
||||||
|
weekStartTimestamp.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const weekEndTimestamp = new Date(weekEndDate);
|
||||||
|
weekEndTimestamp.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const weekShifts = await db
|
||||||
|
.select({
|
||||||
|
shift: shifts,
|
||||||
|
site: sites,
|
||||||
|
})
|
||||||
|
.from(shifts)
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(shifts.startTime, weekStartTimestamp),
|
||||||
|
lte(shifts.startTime, weekEndTimestamp),
|
||||||
|
ne(shifts.status, "cancelled"),
|
||||||
|
eq(sites.location, location as any)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ottieni tutte le assegnazioni dei turni della settimana
|
||||||
|
const shiftIds = weekShifts.map(s => s.shift.id);
|
||||||
|
|
||||||
|
const assignments = shiftIds.length > 0 ? await db
|
||||||
|
.select({
|
||||||
|
assignment: shiftAssignments,
|
||||||
|
guard: guards,
|
||||||
|
shift: shifts,
|
||||||
|
})
|
||||||
|
.from(shiftAssignments)
|
||||||
|
.innerJoin(guards, eq(shiftAssignments.guardId, guards.id))
|
||||||
|
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
||||||
|
.where(
|
||||||
|
sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
// Ottieni veicoli assegnati
|
||||||
|
const vehicleAssignments = weekShifts
|
||||||
|
.filter(s => s.shift.vehicleId)
|
||||||
|
.map(s => s.shift.vehicleId);
|
||||||
|
|
||||||
|
const assignedVehicles = vehicleAssignments.length > 0 ? await db
|
||||||
|
.select()
|
||||||
|
.from(vehicles)
|
||||||
|
.where(
|
||||||
|
sql`${vehicles.id} IN (${sql.join(vehicleAssignments.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
// Costruisci struttura dati per 7 giorni
|
||||||
|
const weekData = [];
|
||||||
|
|
||||||
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||||
|
const currentDay = addDays(parsedWeekStart, dayOffset);
|
||||||
|
const dayStr = format(currentDay, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
const dayStartTimestamp = new Date(dayStr);
|
||||||
|
dayStartTimestamp.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const dayEndTimestamp = new Date(dayStr);
|
||||||
|
dayEndTimestamp.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const sitesData = activeSites.map(({ sites: site, service_types: serviceType }) => {
|
||||||
|
// Trova turni del giorno per questo sito
|
||||||
|
const dayShifts = weekShifts.filter(s =>
|
||||||
|
s.shift.siteId === site.id &&
|
||||||
|
s.shift.startTime >= dayStartTimestamp &&
|
||||||
|
s.shift.startTime <= dayEndTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ottieni assegnazioni guardie per i turni del giorno
|
||||||
|
const dayAssignments = assignments.filter(a =>
|
||||||
|
dayShifts.some(ds => ds.shift.id === a.shift.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calcola ore per ogni guardia
|
||||||
|
const guardsWithHours = dayAssignments.map(a => {
|
||||||
|
const shiftStart = new Date(a.shift.startTime);
|
||||||
|
const shiftEnd = new Date(a.shift.endTime);
|
||||||
|
const hours = differenceInHours(shiftEnd, shiftStart);
|
||||||
|
|
||||||
|
return {
|
||||||
|
guardId: a.guard.id,
|
||||||
|
guardName: a.guard.fullName,
|
||||||
|
badgeNumber: a.guard.badgeNumber,
|
||||||
|
hours,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Veicoli assegnati ai turni del giorno
|
||||||
|
const dayVehicles = dayShifts
|
||||||
|
.filter(ds => ds.shift.vehicleId)
|
||||||
|
.map(ds => {
|
||||||
|
const vehicle = assignedVehicles.find(v => v.id === ds.shift.vehicleId);
|
||||||
|
return vehicle ? {
|
||||||
|
vehicleId: vehicle.id,
|
||||||
|
licensePlate: vehicle.licensePlate,
|
||||||
|
brand: vehicle.brand,
|
||||||
|
model: vehicle.model,
|
||||||
|
} : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Calcolo guardie mancanti
|
||||||
|
// Formula: ceil(24 / maxOreGuardia) × minGuardie - guardieAssegnate
|
||||||
|
const maxOreGuardia = 9; // Max ore per guardia
|
||||||
|
const minGuardie = site.minGuards || 1;
|
||||||
|
|
||||||
|
// Somma ore totali dei turni del giorno
|
||||||
|
const totalShiftHours = dayShifts.reduce((sum, ds) => {
|
||||||
|
const start = new Date(ds.shift.startTime);
|
||||||
|
const end = new Date(ds.shift.endTime);
|
||||||
|
return sum + differenceInHours(end, start);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Slot necessari per coprire le ore totali
|
||||||
|
const slotsNeeded = totalShiftHours > 0 ? Math.ceil(totalShiftHours / maxOreGuardia) : 0;
|
||||||
|
|
||||||
|
// Guardie totali necessarie (slot × min guardie contemporanee)
|
||||||
|
const totalGuardsNeeded = slotsNeeded * minGuardie;
|
||||||
|
|
||||||
|
// Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
|
||||||
|
const uniqueGuardsAssigned = new Set(guardsWithHours.map(g => g.guardId)).size;
|
||||||
|
|
||||||
|
// Guardie mancanti
|
||||||
|
const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
|
||||||
|
|
||||||
|
return {
|
||||||
|
siteId: site.id,
|
||||||
|
siteName: site.name,
|
||||||
|
serviceType: serviceType?.label || "N/A",
|
||||||
|
minGuards: site.minGuards,
|
||||||
|
guards: guardsWithHours,
|
||||||
|
vehicles: dayVehicles,
|
||||||
|
totalShiftHours,
|
||||||
|
guardsAssigned: uniqueGuardsAssigned,
|
||||||
|
missingGuards,
|
||||||
|
shiftsCount: dayShifts.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
weekData.push({
|
||||||
|
date: dayStr,
|
||||||
|
dayOfWeek: format(currentDay, "EEEE"),
|
||||||
|
sites: sitesData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
weekStart: weekStartDate,
|
||||||
|
weekEnd: weekEndDate,
|
||||||
|
location,
|
||||||
|
days: weekData,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching general planning:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch general planning", error: String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============= CERTIFICATION ROUTES =============
|
// ============= CERTIFICATION ROUTES =============
|
||||||
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user