Add operational planning view for uncovered sites

Introduces a new API endpoint `/api/operational-planning/uncovered-sites` that queries for sites not fully covered by assigned guards on a given date, returning sites with partial or no coverage.

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
This commit is contained in:
marco370 2025-10-17 13:52:54 +00:00
parent 144a281657
commit 283b24bcb6

View File

@ -5,7 +5,7 @@ import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit
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 { 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
@ -654,6 +654,97 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// 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 {