- {guardsForShift?.map((guard) => {
+ {filteredGuards?.map((guard) => {
const assigned = isGuardAssigned(guard.id);
const canAssign = canGuardBeAssigned(guard);
- const bookedOnMobile = guard.isBookedMobile;
return (
- {guard.firstName} {guard.lastName}
+ {guard.user?.firstName} {guard.user?.lastName}
#{guard.badgeNumber}
- {bookedOnMobile && (
-
- Su pattuglia mobile
-
- )}
{guard.isArmed && (
@@ -640,10 +615,10 @@ export default function Shifts() {
size="sm"
variant={assigned ? "secondary" : "default"}
onClick={() => handleAssignGuard(guard.id)}
- disabled={assigned || !canAssign || bookedOnMobile}
+ disabled={assigned || !canAssign}
data-testid={`button-assign-guard-${guard.id}`}
>
- {assigned ? "Assegnato" : bookedOnMobile ? "Su pattuglia" : canAssign ? "Assegna" : "Non idoneo"}
+ {assigned ? "Assegnato" : canAssign ? "Assegna" : "Non idoneo"}
);
diff --git a/replit.md b/replit.md
index e15f60b..467b9da 100644
--- a/replit.md
+++ b/replit.md
@@ -142,20 +142,6 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
- Backend endpoint: GET `/api/site-planning/:siteId` with date range filters
- **Impact**: Complete end-to-end planning system supporting both coordinator and guard roles with database-backed route planning and operational equipment tracking
-### Planning Consultation Pages & Sidebar Reorganization (October 23, 2025)
-- **Issue**: Coordinators needed separate consultation views to review planned shifts without mixing creation and consultation workflows. Sidebar was cluttered with deprecated planning pages.
-- **Solution**:
- - **New Consultation Pages**:
- - `planning-view-fixed-agent.tsx`: Weekly view of guard's fixed shifts showing orari, dotazioni (armato, automezzo), location, sito
- - `planning-view-mobile-agent.tsx`: Weekly grid view (7 days) of guard's patrol routes with site addresses, sequenced stops, and equipment
- - Backend endpoint `/api/planning/mobile-agent` updated to accept startDate/endDate range and return `{ guard, days[] }` structure
- - **Sidebar Reorganization**:
- - Removed deprecated routes: `planning`, `operational-planning`, `general-planning`, `service-planning`
- - Created logical groups: Dashboard, Planning-Creazione (Turni Fissi, Pattuglie Mobile), Planning-Consultazione (Planning Agente Fisso, Planning Agente Mobile, Planning Sito), I Miei Turni, Anagrafica, Reporting, Sistema
- - Role-based filtering maintained for all groups
- - **Routes Cleanup**: Removed deprecated planning page imports and routes from App.tsx
-- **Impact**: Clear separation between creation (shifts.tsx, planning-mobile.tsx) and consultation (planning-view-*) workflows. Coordinators can now efficiently review weekly assignments for guards and sites without navigating through creation interfaces.
-
## External Dependencies
- **Replit Auth**: For OpenID Connect (OIDC) based authentication.
- **Neon**: Managed PostgreSQL database service.
diff --git a/server/routes.ts b/server/routes.ts
index a1c28ef..e563a53 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -331,291 +331,8 @@ export async function registerRoutes(app: Express): Promise
{
res.status(500).json({ message: "Failed to fetch guards availability" });
}
});
-
- // Get guards for shift assignment with mobile exclusivity check
- app.get("/api/guards/for-shift", isAuthenticated, async (req, res) => {
- try {
- const { date, location } = req.query;
-
- if (!date || typeof date !== "string") {
- return res.status(400).json({ message: "Date parameter required (YYYY-MM-DD)" });
- }
-
- if (!location || !["roccapiemonte", "milano", "roma"].includes(location as string)) {
- return res.status(400).json({ message: "Valid location parameter required" });
- }
-
- // Valida formato data
- const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
- if (!dateRegex.test(date)) {
- return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" });
- }
-
- // Ottieni tutte le guardie per la location
- const allGuards = await db
- .select({
- id: guards.id,
- userId: guards.userId,
- firstName: guards.firstName,
- lastName: guards.lastName,
- badgeNumber: guards.badgeNumber,
- location: guards.location,
- hasDriverLicense: guards.hasDriverLicense,
- isActive: guards.isActive,
- createdAt: guards.createdAt,
- updatedAt: guards.updatedAt,
- })
- .from(guards)
- .where(
- and(
- eq(guards.location, location as any),
- eq(guards.isActive, true)
- )
- )
- .orderBy(guards.lastName, guards.firstName);
-
- // Verifica quali guardie hanno patrol routes (mobile) per questa data
- const patrolRoutesForDate = await db
- .select({
- guardId: patrolRoutes.guardId,
- })
- .from(patrolRoutes)
- .where(
- and(
- eq(patrolRoutes.shiftDate, date),
- eq(patrolRoutes.location, location as any),
- ne(patrolRoutes.status, "cancelled")
- )
- );
-
- const guardsOnMobile = new Set(patrolRoutesForDate.map(pr => pr.guardId));
-
- // Aggiungi flag isBookedMobile per ogni guardia
- const guardsWithMobileFlag = allGuards.map(guard => ({
- ...guard,
- isBookedMobile: guardsOnMobile.has(guard.id),
- }));
-
- res.json(guardsWithMobileFlag);
- } catch (error) {
- console.error("Error fetching guards for shift:", error);
- res.status(500).json({ message: "Failed to fetch guards for shift" });
- }
- });
-
- // ============= PLANNING CONSULTATION ROUTES =============
- // GET /api/planning/fixed-agent - Vista consultazione planning per agente fisso
- app.get("/api/planning/fixed-agent", isAuthenticated, async (req, res) => {
- try {
- const { guardId, weekStart } = req.query;
-
- if (!guardId || typeof guardId !== "string") {
- return res.status(400).json({ message: "GuardId parameter required" });
- }
-
- if (!weekStart || typeof weekStart !== "string") {
- return res.status(400).json({ message: "WeekStart parameter required (YYYY-MM-DD)" });
- }
-
- // Valida formato data
- const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
- if (!dateRegex.test(weekStart)) {
- return res.status(400).json({ message: "Invalid weekStart format, use YYYY-MM-DD" });
- }
-
- // Calcola fine settimana
- const weekStartDate = new Date(weekStart);
- weekStartDate.setHours(0, 0, 0, 0);
-
- const weekEndDate = new Date(weekStart);
- weekEndDate.setDate(weekEndDate.getDate() + 6);
- weekEndDate.setHours(23, 59, 59, 999);
-
- // Ottieni info guardia
- const guard = await db
- .select({
- id: guards.id,
- firstName: guards.firstName,
- lastName: guards.lastName,
- badgeNumber: guards.badgeNumber,
- location: guards.location,
- })
- .from(guards)
- .where(eq(guards.id, guardId))
- .limit(1);
-
- if (guard.length === 0) {
- return res.status(404).json({ message: "Guard not found" });
- }
-
- // Ottieni tutti i turni fissi della guardia per la settimana
- const assignments = await db
- .select({
- id: shiftAssignments.id,
- shiftId: shifts.id,
- siteId: sites.id,
- siteName: sites.name,
- siteAddress: sites.address,
- startTime: shifts.startTime,
- endTime: shifts.endTime,
- isArmedOnDuty: shiftAssignments.isArmedOnDuty,
- assignedVehicleId: shiftAssignments.assignedVehicleId,
- location: sites.location,
- })
- .from(shiftAssignments)
- .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
- .innerJoin(sites, eq(shifts.siteId, sites.id))
- .where(
- and(
- eq(shiftAssignments.guardId, guardId),
- gte(shifts.startTime, weekStartDate),
- lte(shifts.startTime, weekEndDate)
- )
- )
- .orderBy(shifts.startTime);
-
- res.json({
- guard: guard[0],
- weekStart,
- assignments: assignments.map(a => ({
- id: a.id,
- shiftId: a.shiftId,
- siteId: a.siteId,
- siteName: a.siteName,
- siteAddress: a.siteAddress,
- startTime: a.startTime,
- endTime: a.endTime,
- isArmedOnDuty: a.isArmedOnDuty || false,
- hasVehicle: !!a.assignedVehicleId,
- vehicleId: a.assignedVehicleId,
- location: a.location,
- })),
- });
- } catch (error) {
- console.error("Error fetching fixed agent planning:", error);
- res.status(500).json({ message: "Failed to fetch fixed agent planning" });
- }
- });
-
- // GET /api/planning/mobile-agent - Vista consultazione planning per agente mobile
- app.get("/api/planning/mobile-agent", isAuthenticated, async (req, res) => {
- try {
- const { guardId, startDate, endDate } = req.query;
-
- if (!guardId || typeof guardId !== "string") {
- return res.status(400).json({ message: "GuardId parameter required" });
- }
-
- if (!startDate || typeof startDate !== "string") {
- return res.status(400).json({ message: "StartDate parameter required (YYYY-MM-DD)" });
- }
-
- if (!endDate || typeof endDate !== "string") {
- return res.status(400).json({ message: "EndDate parameter required (YYYY-MM-DD)" });
- }
-
- // Valida formato data
- const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
- if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
- return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" });
- }
-
- // Ottieni info guardia
- const guard = await db
- .select({
- id: guards.id,
- firstName: guards.firstName,
- lastName: guards.lastName,
- badgeNumber: guards.badgeNumber,
- location: guards.location,
- })
- .from(guards)
- .where(eq(guards.id, guardId))
- .limit(1);
-
- if (guard.length === 0) {
- return res.status(404).json({ message: "Guard not found" });
- }
-
- // Ottieni tutti i patrol routes per questa guardia nel range di date
- const routes = await db
- .select({
- id: patrolRoutes.id,
- shiftDate: patrolRoutes.shiftDate,
- startTime: patrolRoutes.startTime,
- endTime: patrolRoutes.endTime,
- location: patrolRoutes.location,
- status: patrolRoutes.status,
- notes: patrolRoutes.notes,
- assignedVehicleId: patrolRoutes.assignedVehicleId,
- })
- .from(patrolRoutes)
- .where(
- and(
- eq(patrolRoutes.guardId, guardId),
- gte(patrolRoutes.shiftDate, startDate),
- lte(patrolRoutes.shiftDate, endDate)
- )
- )
- .orderBy(patrolRoutes.shiftDate);
-
- // Per ogni route, ottieni gli stops
- const days = await Promise.all(
- routes.map(async (route) => {
- const stops = await db
- .select({
- id: patrolRouteStops.id,
- siteId: sites.id,
- siteName: sites.name,
- siteAddress: sites.address,
- latitude: sites.latitude,
- longitude: sites.longitude,
- sequenceOrder: patrolRouteStops.sequenceOrder,
- estimatedArrivalTime: patrolRouteStops.estimatedArrivalTime,
- })
- .from(patrolRouteStops)
- .innerJoin(sites, eq(patrolRouteStops.siteId, sites.id))
- .where(eq(patrolRouteStops.patrolRouteId, route.id))
- .orderBy(patrolRouteStops.sequenceOrder);
-
- return {
- date: route.shiftDate,
- route: {
- id: route.id,
- startTime: route.startTime,
- endTime: route.endTime,
- location: route.location,
- status: route.status,
- notes: route.notes,
- hasVehicle: !!route.assignedVehicleId,
- vehicleId: route.assignedVehicleId,
- },
- stops: stops.map(s => ({
- id: s.id,
- siteId: s.siteId,
- siteName: s.siteName,
- siteAddress: s.siteAddress,
- latitude: s.latitude,
- longitude: s.longitude,
- sequenceOrder: s.sequenceOrder,
- estimatedArrivalTime: s.estimatedArrivalTime,
- })),
- };
- })
- );
-
- res.json({
- guard: guard[0],
- days,
- });
- } catch (error) {
- console.error("Error fetching mobile agent planning:", error);
- res.status(500).json({ message: "Failed to fetch mobile agent planning" });
- }
- });
-
- // GET vehicles available for a location
+ // Get vehicles available for a location
app.get("/api/vehicles/available", isAuthenticated, async (req, res) => {
try {
const { location } = req.query;