Compare commits

...

4 Commits

Author SHA1 Message Date
Marco Lanzara
3a7f44f49f 🚀 Release v1.0.27
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.27_20251021_162727.sql.gz
- Data: 2025-10-21 16:27:43
2025-10-21 16:27:43 +00:00
marco370
4633f6ef5f Improve company settings and access controls for improved security
Modify CompanySettingsController and related services to enhance access control mechanisms and update company settings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/G1ZUdV2
2025-10-21 16:08:27 +00:00
marco370
cd3622a97e Allow removing guards from scheduled shifts
Remove the unassign guard mutation and its associated toast notifications from the GeneralPlanning page, as this functionality is no longer required.

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/G1ZUdV2
2025-10-21 16:07:32 +00:00
marco370
eec694d9d1 Improve shift planning by adding time display and deletion functionality
Adds Italian time formatting, assignment deletion, and displays planned start/end times for guards in the general planning view.

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/G1ZUdV2
2025-10-21 15:52:57 +00:00
5 changed files with 133 additions and 55 deletions

View File

@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus } from "lucide-react"; import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus, Trash2, Clock } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
@ -24,10 +24,13 @@ import { useToast } from "@/hooks/use-toast";
import type { GuardAvailability } from "@shared/schema"; import type { GuardAvailability } from "@shared/schema";
interface GuardWithHours { interface GuardWithHours {
assignmentId: string;
guardId: string; guardId: string;
guardName: string; guardName: string;
badgeNumber: string; badgeNumber: string;
hours: number; hours: number;
plannedStartTime: string;
plannedEndTime: string;
} }
interface Vehicle { interface Vehicle {
@ -68,6 +71,18 @@ interface GeneralPlanningResponse {
}; };
} }
// Helper per formattare orario in formato italiano 24h (HH:MM)
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit", hour12: false });
};
// Helper per formattare data in formato italiano (gg/mm/aaaa)
const formatDateIT = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("it-IT");
};
export default function GeneralPlanning() { export default function GeneralPlanning() {
const [, navigate] = useLocation(); const [, navigate] = useLocation();
const { toast } = useToast(); const { toast } = useToast();
@ -124,6 +139,29 @@ export default function GeneralPlanning() {
enabled: !!selectedCell, // Query attiva solo se dialog è aperto enabled: !!selectedCell, // Query attiva solo se dialog è aperto
}); });
// Mutation per eliminare assegnazione guardia
const deleteAssignmentMutation = useMutation({
mutationFn: async (assignmentId: string) => {
return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, undefined);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
toast({
title: "Guardia rimossa",
description: "L'assegnazione è stata eliminata con successo",
});
},
onError: (error: any) => {
toast({
title: "Errore",
description: "Impossibile eliminare l'assegnazione",
variant: "destructive",
});
},
});
// Mutation per assegnare guardia con orari (anche multi-giorno) // Mutation per assegnare guardia con orari (anche multi-giorno)
const assignGuardMutation = useMutation({ const assignGuardMutation = useMutation({
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number }) => { mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number }) => {
@ -169,29 +207,6 @@ export default function GeneralPlanning() {
}, },
}); });
// Mutation per deassegnare guardia
const unassignGuardMutation = useMutation({
mutationFn: async (assignmentId: string) => {
return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, {});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
toast({
title: "Guardia deassegnata",
description: "La guardia è stata rimossa dal turno",
});
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile deassegnare la guardia",
variant: "destructive",
});
},
});
// Handler per submit form assegnazione guardia // Handler per submit form assegnazione guardia
const handleAssignGuard = () => { const handleAssignGuard = () => {
if (!selectedCell || !selectedGuardId) return; if (!selectedCell || !selectedGuardId) return;
@ -599,11 +614,34 @@ export default function GeneralPlanning() {
<p className="text-xs font-medium text-muted-foreground"> <p className="text-xs font-medium text-muted-foreground">
Guardie già assegnate per questa data: Guardie già assegnate per questa data:
</p> </p>
<div className="grid gap-1.5"> <div className="grid gap-2">
{selectedCell.data.guards.map((guard, idx) => ( {selectedCell.data.guards.map((guard, idx) => (
<div key={idx} className="flex items-center justify-between text-xs bg-background p-2 rounded"> <div key={idx} className="flex items-start justify-between gap-2 bg-background p-2.5 rounded border">
<span className="font-medium">{guard.guardName} <Badge variant="outline" className="ml-1 text-xs">#{guard.badgeNumber}</Badge></span> <div className="flex-1 space-y-1">
<span className="text-muted-foreground">{guard.hours}h</span> <div className="flex items-center gap-2">
<span className="font-medium text-sm">{guard.guardName}</span>
<Badge variant="outline" className="text-xs">#{guard.badgeNumber}</Badge>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>{formatTime(guard.plannedStartTime)} - {formatTime(guard.plannedEndTime)}</span>
<span className="font-medium">({guard.hours}h)</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => {
if (confirm(`Confermi di voler rimuovere ${guard.guardName} da questo turno?`)) {
deleteAssignmentMutation.mutate(guard.assignmentId);
}
}}
disabled={deleteAssignmentMutation.isPending}
data-testid={`button-delete-assignment-${guard.guardId}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div> </div>
))} ))}
</div> </div>

View File

@ -963,17 +963,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
dayShifts.some((ds: any) => ds.shift.id === a.shift.id) dayShifts.some((ds: any) => ds.shift.id === a.shift.id)
); );
// Calcola ore per ogni guardia // Calcola ore per ogni guardia con orari e assignmentId
const guardsWithHours = dayAssignments.map((a: any) => { const guardsWithHours = dayAssignments.map((a: any) => {
const shiftStart = new Date(a.shift.startTime); const plannedStart = new Date(a.assignment.plannedStartTime);
const shiftEnd = new Date(a.shift.endTime); const plannedEnd = new Date(a.assignment.plannedEndTime);
const hours = differenceInHours(shiftEnd, shiftStart); const hours = differenceInHours(plannedEnd, plannedStart);
return { return {
assignmentId: a.assignment.id,
guardId: a.guard.id, guardId: a.guard.id,
guardName: a.guard.fullName, guardName: a.guard.fullName,
badgeNumber: a.guard.badgeNumber, badgeNumber: a.guard.badgeNumber,
hours, hours,
plannedStartTime: a.assignment.plannedStartTime,
plannedEndTime: a.assignment.plannedEndTime,
}; };
}); });
@ -1177,6 +1180,35 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// Delete a shift assignment
app.delete("/api/shift-assignments/:assignmentId", isAuthenticated, async (req, res) => {
try {
const { assignmentId } = req.params;
if (!assignmentId) {
return res.status(400).json({ message: "Assignment ID is required" });
}
// Delete the assignment
const deleted = await db
.delete(shiftAssignments)
.where(eq(shiftAssignments.id, assignmentId))
.returning();
if (deleted.length === 0) {
return res.status(404).json({ message: "Assignment not found" });
}
res.json({
message: "Assignment deleted successfully",
assignment: deleted[0]
});
} catch (error: any) {
console.error("Error deleting assignment:", error);
res.status(500).json({ message: "Failed to delete assignment", error: String(error) });
}
});
// Assign guard to site/date with specific time slot (supports multi-day assignments) // Assign guard to site/date with specific time slot (supports multi-day assignments)
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => { app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
try { try {
@ -1204,7 +1236,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(404).json({ message: "Guard not found" }); return res.status(404).json({ message: "Guard not found" });
} }
// Parse start date WITHOUT timezone conversion (stay in local time) // Parse start date components
const [year, month, day] = date.split("-").map(Number); const [year, month, day] = date.split("-").map(Number);
if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) { if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) {
return res.status(400).json({ message: "Invalid date format. Expected YYYY-MM-DD" }); return res.status(400).json({ message: "Invalid date format. Expected YYYY-MM-DD" });
@ -1217,10 +1249,18 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Loop through each consecutive day // Loop through each consecutive day
for (let dayOffset = 0; dayOffset < consecutiveDays; dayOffset++) { for (let dayOffset = 0; dayOffset < consecutiveDays; dayOffset++) {
// Calculate date for this iteration // Calculate date components for this iteration (avoid timezone issues)
const currentDate = new Date(year, month - 1, day + dayOffset); const targetDay = day + dayOffset;
const shiftDate = new Date(currentDate); const baseDate = new Date(year, month - 1, 1); // First day of month
shiftDate.setHours(0, 0, 0, 0); baseDate.setDate(targetDay); // Set to target day (handles month overflow)
// Extract actual date components after overflow handling
const actualYear = baseDate.getFullYear();
const actualMonth = baseDate.getMonth();
const actualDay = baseDate.getDate();
// Build dates in UTC to avoid timezone shifts
const shiftDate = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0));
// Check contract validity for this date // Check contract validity for this date
if (site.contractStartDate && site.contractEndDate) { if (site.contractStartDate && site.contractEndDate) {
@ -1233,17 +1273,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
} }
// Calculate planned start and end times for this day // Calculate planned start and end times in UTC
const plannedStart = new Date(currentDate); const plannedStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours, minutes, 0, 0));
plannedStart.setHours(hours, minutes, 0, 0); const plannedEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0));
const plannedEnd = new Date(currentDate);
plannedEnd.setHours(hours + durationHours, minutes, 0, 0);
// Find or create shift for this site/date // Find or create shift for this site/date (full day boundaries in UTC)
const dayStart = new Date(shiftDate); const dayStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0));
dayStart.setHours(0, 0, 0, 0); const dayEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, 23, 59, 59, 999));
const dayEnd = new Date(shiftDate);
dayEnd.setHours(23, 59, 59, 999);
let existingShifts = await tx let existingShifts = await tx
.select() .select()
@ -1267,14 +1303,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
const [startHour, startMin] = serviceStart.split(":").map(Number); const [startHour, startMin] = serviceStart.split(":").map(Number);
const [endHour, endMin] = serviceEnd.split(":").map(Number); const [endHour, endMin] = serviceEnd.split(":").map(Number);
const shiftStart = new Date(shiftDate); const shiftStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0));
shiftStart.setHours(startHour, startMin, 0, 0); let shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0));
const shiftEnd = new Date(shiftDate);
shiftEnd.setHours(endHour, endMin, 0, 0);
// If end time is before/equal to start time, shift extends to next day
if (shiftEnd <= shiftStart) { if (shiftEnd <= shiftStart) {
shiftEnd.setDate(shiftEnd.getDate() + 1); shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0));
} }
[shift] = await tx.insert(shifts).values({ [shift] = await tx.insert(shifts).values({

View File

@ -1,7 +1,13 @@
{ {
"version": "1.0.26", "version": "1.0.27",
"lastUpdate": "2025-10-21T15:40:58.930Z", "lastUpdate": "2025-10-21T16:27:43.584Z",
"changelog": [ "changelog": [
{
"version": "1.0.27",
"date": "2025-10-21",
"type": "patch",
"description": "Deployment automatico v1.0.27"
},
{ {
"version": "1.0.26", "version": "1.0.26",
"date": "2025-10-21", "date": "2025-10-21",