Compare commits

...

8 Commits

Author SHA1 Message Date
Marco Lanzara
2b62d8ff4e 🚀 Release v1.0.53
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.53_20251024_163455.sql.gz
- Data: 2025-10-24 16:35:13
2025-10-24 16:35:13 +00:00
marco370
1639244169 Add system to manage guard shifts and client portal functionality
Implement a new API endpoint for guard shift management and a client portal.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:47:54 +00:00
marco370
2cd6c32ad9 Add ability to duplicate shifts and patrol routes to streamline planning
Introduces POST /api/shift-assignments/copy-week and POST /api/patrol-routes/duplicate endpoints for duplicating weekly fixed shifts and mobile patrol routes, respectively, with corresponding frontend dialogs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:47:25 +00:00
marco370
0a72b413fa Add functionality to duplicate weekly schedules and patrol routes
Introduces a dialog to copy weekly schedules to the next week and duplicates patrol routes with specified guards and dates, updating the client-side UI and API interactions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:46:11 +00:00
marco370
1bad21cf9e Add functionality to duplicate and modify patrol routes
Adds a new POST endpoint `/api/patrol-routes/duplicate` to duplicate existing patrol routes to a new date, optionally assigning a different guard. If the target date is the same as the source date, it updates the guard for the existing route.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:40:27 +00:00
marco370
6366382753 Add functionality to copy weekly shifts to the next week
Introduce a new feature allowing users to copy weekly shift assignments to the subsequent week via a dedicated button, including a confirmation dialog and error handling for the copy operation. The UI also includes an update to the navigation bar for better responsiveness.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:38:51 +00:00
marco370
0b64fd2f08 Add functionality to copy weekly shift assignments to the following week
Introduce a new POST API endpoint `/api/shift-assignments/copy-week` to duplicate existing shift assignments and their associated shifts for a specified location and week, automatically adjusting dates by adding 7 days.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:36:54 +00:00
marco370
36bfad3815 Add a new system for managing security guard shift schedules
No changes to review.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EDxr1e6
2025-10-24 15:35:33 +00:00
7 changed files with 631 additions and 14 deletions

View File

@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus, Trash2, Clock } from "lucide-react";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus, Trash2, Clock, Copy } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
@ -117,6 +117,7 @@ export default function GeneralPlanning() {
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
const [ccnlConfirmation, setCcnlConfirmation] = useState<{ message: string; data: any } | null>(null);
const [showCopyWeekConfirmation, setShowCopyWeekConfirmation] = useState<boolean>(false);
// Query per dati planning settimanale
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
@ -275,6 +276,54 @@ export default function GeneralPlanning() {
},
});
// Mutation per copiare turni settimanali
const copyWeekMutation = useMutation({
mutationFn: async () => {
return apiRequest("POST", "/api/shift-assignments/copy-week", {
weekStart: format(weekStart, "yyyy-MM-dd"),
location: selectedLocation,
});
},
onSuccess: async (response: any) => {
const data = await response.json();
toast({
title: "Settimana copiata!",
description: `${data.copiedShifts} turni e ${data.copiedAssignments} assegnazioni copiate nella settimana successiva`,
});
// Invalida cache e naviga alla settimana successiva
await queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
setWeekStart(addWeeks(weekStart, 1)); // Naviga alla settimana copiata
setShowCopyWeekConfirmation(false);
},
onError: (error: any) => {
let errorMessage = "Impossibile copiare la settimana";
if (error.message) {
const match = error.message.match(/^(\d+):\s*(.+)$/);
if (match) {
try {
const parsed = JSON.parse(match[2]);
errorMessage = parsed.message || errorMessage;
} catch {
errorMessage = match[2];
}
} else {
errorMessage = error.message;
}
}
toast({
title: "Errore Copia Settimana",
description: errorMessage,
variant: "destructive",
});
setShowCopyWeekConfirmation(false);
},
});
// Handler per submit form assegnazione guardia
const handleAssignGuard = () => {
if (!selectedCell || !selectedGuardId) return;
@ -358,7 +407,7 @@ export default function GeneralPlanning() {
</div>
{/* Navigazione settimana */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="outline"
size="icon"
@ -385,6 +434,16 @@ export default function GeneralPlanning() {
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="default"
onClick={() => setShowCopyWeekConfirmation(true)}
disabled={isLoading || !planningData || copyWeekMutation.isPending}
data-testid="button-copy-week"
>
<Copy className="h-4 w-4 mr-2" />
{copyWeekMutation.isPending ? "Copia in corso..." : "Copia Turno Settimanale"}
</Button>
</div>
{/* Info settimana */}
@ -988,6 +1047,60 @@ export default function GeneralPlanning() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Dialog conferma copia settimana */}
<AlertDialog open={showCopyWeekConfirmation} onOpenChange={setShowCopyWeekConfirmation}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Copy className="h-5 w-5" />
Copia Turno Settimanale
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
<p className="text-foreground font-medium">
Vuoi copiare tutti i turni della settimana corrente nella settimana successiva?
</p>
{planningData && (
<div className="space-y-2 bg-muted/30 p-3 rounded-md">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Settimana corrente:</span>
<span className="font-medium">
{format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "}
{format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Verrà copiata in:</span>
<span className="font-medium">
{format(addWeeks(new Date(planningData.weekStart), 1), "dd MMM", { locale: it })} -{" "}
{format(addWeeks(new Date(planningData.weekEnd), 1), "dd MMM yyyy", { locale: it })}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Sede:</span>
<span className="font-medium">{formatLocation(selectedLocation)}</span>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
Tutti i turni e le assegnazioni guardie verranno duplicati con le stesse caratteristiche (orari, dotazioni, veicoli).
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-copy-week">Annulla</AlertDialogCancel>
<AlertDialogAction
onClick={() => copyWeekMutation.mutate()}
data-testid="button-confirm-copy-week"
disabled={copyWeekMutation.isPending}
>
{copyWeekMutation.isPending ? "Copia in corso..." : "Conferma Copia"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@ -6,8 +6,16 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered } from "lucide-react";
import { format, parseISO, isValid } from "date-fns";
import { Calendar, MapPin, User, Car, Clock, Navigation, ListOrdered, Copy } from "lucide-react";
import { format, parseISO, isValid, addDays } from "date-fns";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { it } from "date-fns/locale";
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
@ -87,6 +95,19 @@ export default function PlanningMobile() {
const [mapZoom, setMapZoom] = useState(12);
const [patrolRoute, setPatrolRoute] = useState<MobileSite[]>([]);
// State per dialog duplicazione sequenza
const [duplicateDialog, setDuplicateDialog] = useState<{
isOpen: boolean;
sourceRoute: any | null;
targetDate: string;
selectedDuplicateGuardId: string;
}>({
isOpen: false,
sourceRoute: null,
targetDate: "",
selectedDuplicateGuardId: "",
});
// Query siti mobile per location
const { data: mobileSites, isLoading: sitesLoading } = useQuery<MobileSite[]>({
queryKey: ["/api/planning-mobile/sites", selectedLocation],
@ -131,6 +152,55 @@ export default function PlanningMobile() {
enabled: !!selectedDate && !!selectedLocation,
});
// Mutation per duplicare sequenza pattuglia
const duplicatePatrolRouteMutation = useMutation({
mutationFn: async (data: { sourceRouteId: string; targetDate: string; guardId: string }) => {
return apiRequest("POST", "/api/patrol-routes/duplicate", data);
},
onSuccess: async (response: any) => {
const data = await response.json();
const actionLabel = data.action === "updated" ? "modificata" : "duplicata";
toast({
title: `Sequenza ${actionLabel}!`,
description: data.message,
});
// Invalida cache e chiudi dialog
await queryClient.invalidateQueries({ queryKey: ["/api/patrol-routes"] });
setDuplicateDialog({
isOpen: false,
sourceRoute: null,
targetDate: "",
selectedDuplicateGuardId: "",
});
},
onError: (error: any) => {
let errorMessage = "Impossibile duplicare la sequenza";
if (error.message) {
const match = error.message.match(/^(\d+):\s*(.+)$/);
if (match) {
try {
const parsed = JSON.parse(match[2]);
errorMessage = parsed.message || errorMessage;
} catch {
errorMessage = match[2];
}
} else {
errorMessage = error.message;
}
}
toast({
title: "Errore Duplicazione",
description: errorMessage,
variant: "destructive",
});
},
});
const locationLabels: Record<Location, string> = {
roccapiemonte: "Roccapiemonte",
milano: "Milano",
@ -201,6 +271,35 @@ export default function PlanningMobile() {
});
};
// Funzione per aprire dialog duplicazione sequenza
const handleOpenDuplicateDialog = (route: any) => {
const nextDay = format(addDays(parseISO(selectedDate), 1), "yyyy-MM-dd");
setDuplicateDialog({
isOpen: true,
sourceRoute: route,
targetDate: nextDay, // Default = giorno successivo
selectedDuplicateGuardId: route.guardId, // Pre-compilato con guardia attuale
});
};
// Handler submit dialog duplicazione
const handleSubmitDuplicate = () => {
if (!duplicateDialog.sourceRoute || !duplicateDialog.targetDate || !duplicateDialog.selectedDuplicateGuardId) {
toast({
title: "Campi mancanti",
description: "Compila tutti i campi obbligatori",
variant: "destructive",
});
return;
}
duplicatePatrolRouteMutation.mutate({
sourceRouteId: duplicateDialog.sourceRoute.id,
targetDate: duplicateDialog.targetDate,
guardId: duplicateDialog.selectedDuplicateGuardId,
});
};
// Funzione per aggiungere sito alla patrol route
const handleAddToRoute = (site: MobileSite) => {
if (!selectedGuard) {
@ -667,6 +766,193 @@ export default function PlanningMobile() {
</div>
</CardContent>
</Card>
{/* Sequenze Pattuglia del Giorno */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ListOrdered className="h-5 w-5" />
Sequenze Pattuglia - {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
</CardTitle>
<CardDescription>
Sequenze programmate per la data selezionata
</CardDescription>
</CardHeader>
<CardContent>
{existingPatrolRoutes && existingPatrolRoutes.length > 0 ? (
<div className="space-y-3">
{existingPatrolRoutes.map((route) => {
const guard = availableGuards?.find(g => g.id === route.guardId);
return (
<div
key={route.id}
className="p-4 border rounded-lg space-y-3 hover-elevate"
data-testid={`patrol-route-${route.id}`}
>
<div className="flex items-start justify-between">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span className="font-semibold">
{guard ? `${guard.firstName} ${guard.lastName}` : "Guardia sconosciuta"}
</span>
{guard && (
<Badge variant="outline" className="text-xs">
#{guard.badgeNumber}
</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{route.startTime} - {route.endTime}
</div>
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{route.stops?.length || 0} {route.stops?.length === 1 ? "tappa" : "tappe"}
</div>
</div>
{route.stops && route.stops.length > 0 && (
<div className="text-xs text-muted-foreground mt-2">
<span className="font-medium">Sequenza: </span>
{route.stops.map((stop: any, idx: number) => (
<span key={stop.id}>
{stop.siteName}{idx < route.stops.length - 1 ? " → " : ""}
</span>
))}
</div>
)}
</div>
<Button
size="sm"
variant="outline"
onClick={() => handleOpenDuplicateDialog(route)}
data-testid={`button-duplicate-route-${route.id}`}
>
<Copy className="h-4 w-4 mr-2" />
Duplica
</Button>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 space-y-2">
<ListOrdered className="h-12 w-12 mx-auto text-muted-foreground opacity-50" />
<p className="text-sm text-muted-foreground">
Nessuna sequenza pattuglia pianificata per {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
</p>
</div>
)}
</CardContent>
</Card>
{/* Dialog Duplica Sequenza */}
<Dialog open={duplicateDialog.isOpen} onOpenChange={(open) => {
if (!open) {
setDuplicateDialog({
isOpen: false,
sourceRoute: null,
targetDate: "",
selectedDuplicateGuardId: "",
});
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Copy className="h-5 w-5" />
Duplica/Modifica Sequenza Pattuglia
</DialogTitle>
<DialogDescription>
Copia la sequenza in un'altra data o modifica la guardia assegnata
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Data Sorgente (readonly) */}
{duplicateDialog.sourceRoute && (
<div className="space-y-2">
<Label>Sequenza Sorgente</Label>
<div className="p-3 bg-muted/30 rounded-md space-y-1 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Data:</span>
<span className="font-medium">
{format(parseISO(duplicateDialog.sourceRoute.scheduledDate), "dd/MM/yyyy", { locale: it })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Tappe:</span>
<span className="font-medium">{duplicateDialog.sourceRoute.stops?.length || 0}</span>
</div>
</div>
</div>
)}
{/* Data Target */}
<div className="space-y-2">
<Label htmlFor="target-date">Data di Destinazione *</Label>
<Input
id="target-date"
type="date"
value={duplicateDialog.targetDate}
onChange={(e) => setDuplicateDialog({ ...duplicateDialog, targetDate: e.target.value })}
data-testid="input-target-date"
/>
<p className="text-xs text-muted-foreground">
{duplicateDialog.sourceRoute && duplicateDialog.targetDate &&
format(parseISO(duplicateDialog.sourceRoute.scheduledDate), "yyyy-MM-dd") === duplicateDialog.targetDate
? "⚠️ Stessa data: verrà modificata la guardia della sequenza esistente"
: "✓ Data diversa: verrà creata una nuova sequenza con tutte le tappe"
}
</p>
</div>
{/* Selezione Guardia */}
<div className="space-y-2">
<Label htmlFor="guard-select">Guardia Assegnata *</Label>
<Select
value={duplicateDialog.selectedDuplicateGuardId}
onValueChange={(value) => setDuplicateDialog({ ...duplicateDialog, selectedDuplicateGuardId: value })}
>
<SelectTrigger id="guard-select" data-testid="select-duplicate-guard">
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
<SelectContent>
{availableGuards?.map((guard) => (
<SelectItem key={guard.id} value={guard.id}>
{guard.firstName} {guard.lastName} (#{guard.badgeNumber})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDuplicateDialog({
isOpen: false,
sourceRoute: null,
targetDate: "",
selectedDuplicateGuardId: "",
})}
data-testid="button-cancel-duplicate"
>
Annulla
</Button>
<Button
onClick={handleSubmitDuplicate}
disabled={duplicatePatrolRouteMutation.isPending}
data-testid="button-confirm-duplicate"
>
{duplicatePatrolRouteMutation.isPending ? "Elaborazione..." : "Conferma"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -34,8 +34,8 @@ The database supports managing users, guards, certifications, sites, shifts, shi
### Core Features
- **Multi-Sede Operational Planning**: Location-first approach for shift planning, filtering resources by selected branch.
- **Service Type Classification**: Classifies services as "fisso" (fixed posts) or "mobile" (patrols, inspections) to route sites to appropriate planning modules.
- **Planning Fissi**: Weekly planning grid for fixed posts, enabling shift creation with guard availability checks.
- **Planning Mobile**: Guard-centric interface for mobile services, displaying guard availability and hours, with an interactive Leaflet map showing sites.
- **Planning Fissi**: Weekly planning grid for fixed posts, enabling shift creation with guard availability checks. Includes weekly shift duplication feature with confirmation dialog and automatic navigation.
- **Planning Mobile**: Guard-centric interface for mobile services, displaying guard availability and hours, with an interactive Leaflet map showing sites. Includes patrol sequence list view and duplication/modification dialog.
- **Customer Management**: Full CRUD operations for customer details and customer-centric reporting with CSV export.
- **Dashboard Operativa**: Live KPIs and real-time shift status.
- **Gestione Guardie**: Complete profiles with skill matrix, certification management, and badge numbers.
@ -44,6 +44,9 @@ The database supports managing users, guards, certifications, sites, shifts, shi
- **Advanced Planning**: Management of guard constraints, site preferences, contract parameters, training courses, holidays, and absences. Includes patrol route persistence and exclusivity constraints between fixed and mobile shifts.
- **Guard Planning Views**: Dedicated views for guards to see their fixed post shifts and mobile patrol routes.
- **Site Planning View**: Coordinators can view all guards assigned to a specific site over a week.
- **Shift Duplication Features**:
- **Weekly Copy (Planning Fissi)**: POST /api/shift-assignments/copy-week endpoint duplicates all shifts and assignments from selected week to next week (+7 days) with atomic transaction. Frontend includes confirmation dialog with week details and success feedback.
- **Patrol Sequence Duplication (Planning Mobili)**: POST /api/patrol-routes/duplicate endpoint with dual behavior: UPDATE when target date = source date (modifies guard), CREATE when different date (duplicates route with all stops). Frontend shows daily sequence list with duplication dialog (date picker defaulting to next day, guard selector pre-filled but changeable).
### User Roles
- **Admin**: Full access.

View File

@ -1337,6 +1337,129 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Copy weekly shift assignments to next week
app.post("/api/shift-assignments/copy-week", isAuthenticated, async (req, res) => {
try {
const { weekStart, location } = req.body;
if (!weekStart || !location) {
return res.status(400).json({ message: "Missing required fields: weekStart, location" });
}
// Parse week start date
const [year, month, day] = weekStart.split("-").map(Number);
if (!year || !month || !day) {
return res.status(400).json({ message: "Invalid weekStart format. Expected YYYY-MM-DD" });
}
// Calculate week boundaries (Monday to Sunday)
const weekStartDate = new Date(year, month - 1, day, 0, 0, 0, 0);
const weekEndDate = new Date(year, month - 1, day + 6, 23, 59, 59, 999);
console.log("📋 Copying weekly shifts:", {
weekStart: weekStartDate.toISOString(),
weekEnd: weekEndDate.toISOString(),
location
});
// Transaction: copy all shifts and assignments
const result = await db.transaction(async (tx) => {
// 1. Find all shifts in the source week filtered by location
const sourceShifts = await tx
.select({
shift: shifts,
site: sites
})
.from(shifts)
.innerJoin(sites, eq(shifts.siteId, sites.id))
.where(
and(
gte(shifts.startTime, weekStartDate),
lte(shifts.startTime, weekEndDate),
eq(sites.location, location)
)
);
if (sourceShifts.length === 0) {
throw new Error("Nessun turno trovato nella settimana selezionata");
}
console.log(`📋 Found ${sourceShifts.length} shifts to copy`);
let copiedShiftsCount = 0;
let copiedAssignmentsCount = 0;
// 2. For each shift, copy to next week (+7 days)
for (const { shift: sourceShift, site } of sourceShifts) {
// Calculate new dates (+7 days)
const newStartTime = new Date(sourceShift.startTime);
newStartTime.setDate(newStartTime.getDate() + 7);
const newEndTime = new Date(sourceShift.endTime);
newEndTime.setDate(newEndTime.getDate() + 7);
// Create new shift
const [newShift] = await tx
.insert(shifts)
.values({
siteId: sourceShift.siteId,
startTime: newStartTime,
endTime: newEndTime,
status: "planned",
vehicleId: sourceShift.vehicleId,
notes: sourceShift.notes,
})
.returning();
copiedShiftsCount++;
// 3. Copy all assignments for this shift
const sourceAssignments = await tx
.select()
.from(shiftAssignments)
.where(eq(shiftAssignments.shiftId, sourceShift.id));
for (const sourceAssignment of sourceAssignments) {
// Calculate new planned times (+7 days)
const newPlannedStart = new Date(sourceAssignment.plannedStartTime);
newPlannedStart.setDate(newPlannedStart.getDate() + 7);
const newPlannedEnd = new Date(sourceAssignment.plannedEndTime);
newPlannedEnd.setDate(newPlannedEnd.getDate() + 7);
// Create new assignment
await tx
.insert(shiftAssignments)
.values({
shiftId: newShift.id,
guardId: sourceAssignment.guardId,
plannedStartTime: newPlannedStart,
plannedEndTime: newPlannedEnd,
isArmedOnDuty: sourceAssignment.isArmedOnDuty,
assignedVehicleId: sourceAssignment.assignedVehicleId,
});
copiedAssignmentsCount++;
}
}
return { copiedShiftsCount, copiedAssignmentsCount };
});
res.json({
message: `Settimana copiata con successo: ${result.copiedShiftsCount} turni, ${result.copiedAssignmentsCount} assegnazioni`,
copiedShifts: result.copiedShiftsCount,
copiedAssignments: result.copiedAssignmentsCount,
});
} catch (error: any) {
console.error("❌ Error copying weekly shifts:", error);
res.status(500).json({
message: error.message || "Errore durante la copia dei turni settimanali",
error: String(error)
});
}
});
// Assign guard to site/date with specific time slot (supports multi-day assignments)
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
try {
@ -4133,6 +4256,98 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// POST - Duplica o modifica patrol route
app.post("/api/patrol-routes/duplicate", isAuthenticated, async (req: any, res) => {
try {
const { sourceRouteId, targetDate, guardId } = req.body;
if (!sourceRouteId || !targetDate) {
return res.status(400).json({
message: "sourceRouteId e targetDate sono obbligatori"
});
}
// Carica patrol route sorgente con tutti gli stops
const sourceRoute = await db.query.patrolRoutes.findFirst({
where: eq(patrolRoutes.id, sourceRouteId),
with: {
stops: {
orderBy: (stops, { asc }) => [asc(stops.sequenceOrder)],
},
},
});
if (!sourceRoute) {
return res.status(404).json({ message: "Sequenza pattuglia sorgente non trovata" });
}
// Controlla se targetDate è uguale a sourceRoute.scheduledDate
const sourceDate = new Date(sourceRoute.scheduledDate).toISOString().split('T')[0];
const targetDateNormalized = new Date(targetDate).toISOString().split('T')[0];
if (sourceDate === targetDateNormalized) {
// UPDATE: stessa data, modifica solo guardia se fornita
if (guardId && guardId !== sourceRoute.guardId) {
const updated = await db
.update(patrolRoutes)
.set({ guardId })
.where(eq(patrolRoutes.id, sourceRouteId))
.returning();
return res.json({
action: "updated",
route: updated[0],
message: "Guardia assegnata alla sequenza esistente",
});
} else {
return res.status(400).json({
message: "Nessuna modifica da applicare (stessa data e stessa guardia)"
});
}
} else {
// CREATE: data diversa, duplica sequenza con stops
// Crea nuova patrol route
const newRoute = await db
.insert(patrolRoutes)
.values({
guardId: guardId || sourceRoute.guardId, // Usa nuova guardia o mantieni originale
scheduledDate: new Date(targetDate),
startTime: sourceRoute.startTime,
endTime: sourceRoute.endTime,
status: "scheduled", // Nuova sequenza sempre in stato scheduled
location: sourceRoute.location,
notes: sourceRoute.notes,
})
.returning();
const newRouteId = newRoute[0].id;
// Duplica tutti gli stops
if (sourceRoute.stops && sourceRoute.stops.length > 0) {
const stopsData = sourceRoute.stops.map((stop) => ({
patrolRouteId: newRouteId,
siteId: stop.siteId,
sequenceOrder: stop.sequenceOrder,
estimatedArrivalTime: stop.estimatedArrivalTime,
}));
await db.insert(patrolRouteStops).values(stopsData);
}
return res.json({
action: "created",
route: newRoute[0],
copiedStops: sourceRoute.stops?.length || 0,
message: "Sequenza pattuglia duplicata con successo",
});
}
} catch (error) {
console.error("Error duplicating patrol route:", error);
res.status(500).json({ message: "Errore durante duplicazione sequenza pattuglia" });
}
});
// ============= GEOCODING API (Nominatim/OSM) =============
// Rate limiter semplice per rispettare 1 req/sec di Nominatim

View File

@ -1,7 +1,13 @@
{
"version": "1.0.52",
"lastUpdate": "2025-10-24T14:53:47.910Z",
"version": "1.0.53",
"lastUpdate": "2025-10-24T16:35:13.318Z",
"changelog": [
{
"version": "1.0.53",
"date": "2025-10-24",
"type": "patch",
"description": "Deployment automatico v1.0.53"
},
{
"version": "1.0.52",
"date": "2025-10-24",
@ -295,12 +301,6 @@
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.4"
},
{
"version": "1.0.3",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.3"
}
]
}