Compare commits
No commits in common. "3a7f44f49ff16a027397e4205f8fc5fcbcbcc63a" and "dd7adeaa2413f5a4442507a7e5688cc844b2dd84" have entirely different histories.
3a7f44f49f
...
dd7adeaa24
@ -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, Trash2, Clock } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus } 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,13 +24,10 @@ 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 {
|
||||||
@ -71,18 +68,6 @@ 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();
|
||||||
@ -139,29 +124,6 @@ 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 }) => {
|
||||||
@ -207,6 +169,29 @@ 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;
|
||||||
@ -614,34 +599,11 @@ 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-2">
|
<div className="grid gap-1.5">
|
||||||
{selectedCell.data.guards.map((guard, idx) => (
|
{selectedCell.data.guards.map((guard, idx) => (
|
||||||
<div key={idx} className="flex items-start justify-between gap-2 bg-background p-2.5 rounded border">
|
<div key={idx} className="flex items-center justify-between text-xs bg-background p-2 rounded">
|
||||||
<div className="flex-1 space-y-1">
|
<span className="font-medium">{guard.guardName} <Badge variant="outline" className="ml-1 text-xs">#{guard.badgeNumber}</Badge></span>
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-muted-foreground">{guard.hours}h</span>
|
||||||
<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>
|
||||||
|
|||||||
BIN
database-backups/vigilanzaturni_v1.0.17_20251017_154110.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.17_20251017_154110.sql.gz
Normal file
Binary file not shown.
Binary file not shown.
@ -963,20 +963,17 @@ 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 con orari e assignmentId
|
// Calcola ore per ogni guardia
|
||||||
const guardsWithHours = dayAssignments.map((a: any) => {
|
const guardsWithHours = dayAssignments.map((a: any) => {
|
||||||
const plannedStart = new Date(a.assignment.plannedStartTime);
|
const shiftStart = new Date(a.shift.startTime);
|
||||||
const plannedEnd = new Date(a.assignment.plannedEndTime);
|
const shiftEnd = new Date(a.shift.endTime);
|
||||||
const hours = differenceInHours(plannedEnd, plannedStart);
|
const hours = differenceInHours(shiftEnd, shiftStart);
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1180,35 +1177,6 @@ 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 {
|
||||||
@ -1236,7 +1204,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 components
|
// Parse start date WITHOUT timezone conversion (stay in local time)
|
||||||
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" });
|
||||||
@ -1249,18 +1217,10 @@ 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 components for this iteration (avoid timezone issues)
|
// Calculate date for this iteration
|
||||||
const targetDay = day + dayOffset;
|
const currentDate = new Date(year, month - 1, day + dayOffset);
|
||||||
const baseDate = new Date(year, month - 1, 1); // First day of month
|
const shiftDate = new Date(currentDate);
|
||||||
baseDate.setDate(targetDay); // Set to target day (handles month overflow)
|
shiftDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -1273,13 +1233,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate planned start and end times in UTC
|
// Calculate planned start and end times for this day
|
||||||
const plannedStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours, minutes, 0, 0));
|
const plannedStart = new Date(currentDate);
|
||||||
const plannedEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0));
|
plannedStart.setHours(hours, minutes, 0, 0);
|
||||||
|
const plannedEnd = new Date(currentDate);
|
||||||
|
plannedEnd.setHours(hours + durationHours, minutes, 0, 0);
|
||||||
|
|
||||||
// Find or create shift for this site/date (full day boundaries in UTC)
|
// Find or create shift for this site/date
|
||||||
const dayStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0));
|
const dayStart = new Date(shiftDate);
|
||||||
const dayEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, 23, 59, 59, 999));
|
dayStart.setHours(0, 0, 0, 0);
|
||||||
|
const dayEnd = new Date(shiftDate);
|
||||||
|
dayEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
let existingShifts = await tx
|
let existingShifts = await tx
|
||||||
.select()
|
.select()
|
||||||
@ -1303,12 +1267,14 @@ 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(Date.UTC(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0));
|
const shiftStart = new Date(shiftDate);
|
||||||
let shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0));
|
shiftStart.setHours(startHour, startMin, 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 = new Date(Date.UTC(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0));
|
shiftEnd.setDate(shiftEnd.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[shift] = await tx.insert(shifts).values({
|
[shift] = await tx.insert(shifts).values({
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.27",
|
"version": "1.0.26",
|
||||||
"lastUpdate": "2025-10-21T16:27:43.584Z",
|
"lastUpdate": "2025-10-21T15:40:58.930Z",
|
||||||
"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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user