Compare commits
No commits in common. "dd7adeaa2413f5a4442507a7e5688cc844b2dd84" and "62f8189e7dbe116868e447dbf29004e1cc25f455" have entirely different histories.
dd7adeaa24
...
62f8189e7d
4
.replit
4
.replit
@ -31,10 +31,6 @@ externalPort = 4200
|
||||
localPort = 42175
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 43169
|
||||
externalPort = 5000
|
||||
|
||||
[[ports]]
|
||||
localPort = 43267
|
||||
externalPort = 3003
|
||||
|
||||
@ -79,7 +79,6 @@ export default function GeneralPlanning() {
|
||||
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
||||
const [startTime, setStartTime] = useState<string>("06:00");
|
||||
const [durationHours, setDurationHours] = useState<number>(8);
|
||||
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
||||
|
||||
// Query per dati planning settimanale
|
||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||
@ -124,9 +123,9 @@ export default function GeneralPlanning() {
|
||||
enabled: !!selectedCell, // Query attiva solo se dialog è aperto
|
||||
});
|
||||
|
||||
// Mutation per assegnare guardia con orari (anche multi-giorno)
|
||||
// Mutation per assegnare guardia con orari
|
||||
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 }) => {
|
||||
return apiRequest("POST", "/api/general-planning/assign-guard", data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
@ -202,7 +201,6 @@ export default function GeneralPlanning() {
|
||||
guardId: selectedGuardId,
|
||||
startTime,
|
||||
durationHours,
|
||||
consecutiveDays,
|
||||
});
|
||||
};
|
||||
|
||||
@ -495,13 +493,7 @@ export default function GeneralPlanning() {
|
||||
</Card>
|
||||
|
||||
{/* Dialog dettagli cella */}
|
||||
<Dialog open={!!selectedCell} onOpenChange={() => {
|
||||
setSelectedCell(null);
|
||||
setSelectedGuardId("");
|
||||
setStartTime("06:00");
|
||||
setDurationHours(8);
|
||||
setConsecutiveDays(1);
|
||||
}}>
|
||||
<Dialog open={!!selectedCell} onOpenChange={() => setSelectedCell(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
@ -593,31 +585,14 @@ export default function GeneralPlanning() {
|
||||
|
||||
{/* Form assegnazione guardia */}
|
||||
<div className="border-t pt-4 space-y-4">
|
||||
{/* Mostra guardie già assegnate per questo giorno */}
|
||||
{selectedCell.data.guards.length > 0 && (
|
||||
<div className="bg-muted/30 p-3 rounded-md space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Guardie già assegnate per questa data:
|
||||
</p>
|
||||
<div className="grid gap-1.5">
|
||||
{selectedCell.data.guards.map((guard, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between text-xs bg-background p-2 rounded">
|
||||
<span className="font-medium">{guard.guardName} <Badge variant="outline" className="ml-1 text-xs">#{guard.badgeNumber}</Badge></span>
|
||||
<span className="text-muted-foreground">{guard.hours}h</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Plus className="h-4 w-4" />
|
||||
Assegna Nuova Guardia
|
||||
Assegna Guardia
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{/* Ora Inizio, Durata e Giorni */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* Ora Inizio e Durata */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="start-time">Ora Inizio</Label>
|
||||
<Input
|
||||
@ -642,19 +617,6 @@ export default function GeneralPlanning() {
|
||||
data-testid="input-duration"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="consecutive-days">Giorni</Label>
|
||||
<Input
|
||||
id="consecutive-days"
|
||||
type="number"
|
||||
min={1}
|
||||
max={30}
|
||||
value={consecutiveDays}
|
||||
onChange={(e) => setConsecutiveDays(Math.max(1, Math.min(30, parseInt(e.target.value) || 1)))}
|
||||
disabled={assignGuardMutation.isPending}
|
||||
data-testid="input-consecutive-days"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ora fine calcolata */}
|
||||
|
||||
BIN
database-backups/vigilanzaturni_v1.0.16_20251017_151927.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.16_20251017_151927.sql.gz
Normal file
Binary file not shown.
Binary file not shown.
207
server/routes.ts
207
server/routes.ts
@ -1177,10 +1177,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Assign guard to site/date with specific time slot (supports multi-day assignments)
|
||||
// Assign guard to site/date with specific time slot
|
||||
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { siteId, date, guardId, startTime, durationHours, consecutiveDays = 1 } = req.body;
|
||||
const { siteId, date, guardId, startTime, durationHours } = req.body;
|
||||
|
||||
if (!siteId || !date || !guardId || !startTime || !durationHours) {
|
||||
return res.status(400).json({
|
||||
@ -1188,10 +1188,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
});
|
||||
}
|
||||
|
||||
if (consecutiveDays < 1 || consecutiveDays > 30) {
|
||||
return res.status(400).json({ message: "consecutiveDays must be between 1 and 30" });
|
||||
}
|
||||
|
||||
// Get site to check contract and service details
|
||||
const site = await storage.getSite(siteId);
|
||||
if (!site) {
|
||||
@ -1204,124 +1200,113 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
return res.status(404).json({ message: "Guard not found" });
|
||||
}
|
||||
|
||||
// Parse start date WITHOUT timezone conversion (stay in local time)
|
||||
const [year, month, day] = date.split("-").map(Number);
|
||||
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" });
|
||||
// Parse date and time
|
||||
const shiftDate = parseISO(date);
|
||||
if (!isValid(shiftDate)) {
|
||||
return res.status(400).json({ message: "Invalid date format" });
|
||||
}
|
||||
const [hours, minutes] = startTime.split(":").map(Number);
|
||||
|
||||
// Atomic transaction: create assignments for all consecutive days
|
||||
// Calculate planned start and end times
|
||||
const [hours, minutes] = startTime.split(":").map(Number);
|
||||
const plannedStart = new Date(shiftDate);
|
||||
plannedStart.setHours(hours, minutes, 0, 0);
|
||||
|
||||
const plannedEnd = new Date(plannedStart);
|
||||
plannedEnd.setHours(plannedStart.getHours() + durationHours);
|
||||
|
||||
// Check contract validity
|
||||
if (site.contractStartDate && site.contractEndDate) {
|
||||
const contractStart = new Date(site.contractStartDate);
|
||||
const contractEnd = new Date(site.contractEndDate);
|
||||
if (shiftDate < contractStart || shiftDate > contractEnd) {
|
||||
return res.status(400).json({
|
||||
message: `Cannot assign guard: date outside contract period`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic transaction: find/create shift + verify no overlaps + create assignment
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const createdAssignments = [];
|
||||
// Find or create shift for this site/date
|
||||
const dayStart = new Date(shiftDate);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(shiftDate);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
// Loop through each consecutive day
|
||||
for (let dayOffset = 0; dayOffset < consecutiveDays; dayOffset++) {
|
||||
// Calculate date for this iteration
|
||||
const currentDate = new Date(year, month - 1, day + dayOffset);
|
||||
const shiftDate = new Date(currentDate);
|
||||
shiftDate.setHours(0, 0, 0, 0);
|
||||
let existingShifts = await tx
|
||||
.select()
|
||||
.from(shifts)
|
||||
.where(
|
||||
and(
|
||||
eq(shifts.siteId, siteId),
|
||||
gte(shifts.startTime, dayStart),
|
||||
lte(shifts.startTime, dayEnd)
|
||||
)
|
||||
);
|
||||
|
||||
let shift;
|
||||
if (existingShifts.length > 0) {
|
||||
shift = existingShifts[0];
|
||||
} else {
|
||||
// Create new shift for full service period
|
||||
const serviceStart = site.serviceStartTime || "00:00";
|
||||
const serviceEnd = site.serviceEndTime || "23:59";
|
||||
|
||||
// Check contract validity for this date
|
||||
if (site.contractStartDate && site.contractEndDate) {
|
||||
const contractStart = new Date(site.contractStartDate);
|
||||
const contractEnd = new Date(site.contractEndDate);
|
||||
if (shiftDate < contractStart || shiftDate > contractEnd) {
|
||||
throw new Error(
|
||||
`Cannot assign guard for date ${shiftDate.toLocaleDateString()}: outside contract period`
|
||||
);
|
||||
}
|
||||
const [startHour, startMin] = serviceStart.split(":").map(Number);
|
||||
const [endHour, endMin] = serviceEnd.split(":").map(Number);
|
||||
|
||||
const shiftStart = new Date(shiftDate);
|
||||
shiftStart.setHours(startHour, startMin, 0, 0);
|
||||
|
||||
const shiftEnd = new Date(shiftDate);
|
||||
shiftEnd.setHours(endHour, endMin, 0, 0);
|
||||
|
||||
if (shiftEnd <= shiftStart) {
|
||||
shiftEnd.setDate(shiftEnd.getDate() + 1);
|
||||
}
|
||||
|
||||
// Calculate planned start and end times for this day
|
||||
const plannedStart = new Date(currentDate);
|
||||
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
|
||||
const dayStart = new Date(shiftDate);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(shiftDate);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
let existingShifts = await tx
|
||||
.select()
|
||||
.from(shifts)
|
||||
.where(
|
||||
and(
|
||||
eq(shifts.siteId, siteId),
|
||||
gte(shifts.startTime, dayStart),
|
||||
lte(shifts.startTime, dayEnd)
|
||||
)
|
||||
);
|
||||
|
||||
let shift;
|
||||
if (existingShifts.length > 0) {
|
||||
shift = existingShifts[0];
|
||||
} else {
|
||||
// Create new shift for full service period
|
||||
const serviceStart = site.serviceStartTime || "00:00";
|
||||
const serviceEnd = site.serviceEndTime || "23:59";
|
||||
|
||||
const [startHour, startMin] = serviceStart.split(":").map(Number);
|
||||
const [endHour, endMin] = serviceEnd.split(":").map(Number);
|
||||
|
||||
const shiftStart = new Date(shiftDate);
|
||||
shiftStart.setHours(startHour, startMin, 0, 0);
|
||||
|
||||
const shiftEnd = new Date(shiftDate);
|
||||
shiftEnd.setHours(endHour, endMin, 0, 0);
|
||||
|
||||
if (shiftEnd <= shiftStart) {
|
||||
shiftEnd.setDate(shiftEnd.getDate() + 1);
|
||||
}
|
||||
|
||||
[shift] = await tx.insert(shifts).values({
|
||||
siteId: site.id,
|
||||
startTime: shiftStart,
|
||||
endTime: shiftEnd,
|
||||
shiftType: site.shiftType || "fixed_post",
|
||||
status: "planned",
|
||||
}).returning();
|
||||
}
|
||||
|
||||
// Recheck overlaps within transaction for this day
|
||||
const existingAssignments = await tx
|
||||
.select()
|
||||
.from(shiftAssignments)
|
||||
.where(eq(shiftAssignments.guardId, guard.id));
|
||||
|
||||
for (const existing of existingAssignments) {
|
||||
const hasOverlap =
|
||||
plannedStart < existing.plannedEndTime &&
|
||||
plannedEnd > existing.plannedStartTime;
|
||||
|
||||
if (hasOverlap) {
|
||||
throw new Error(
|
||||
`Conflitto: guardia già assegnata ${existing.plannedStartTime.toLocaleString()} - ${existing.plannedEndTime.toLocaleString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create assignment for this day
|
||||
const [assignment] = await tx.insert(shiftAssignments).values({
|
||||
shiftId: shift.id,
|
||||
guardId: guard.id,
|
||||
plannedStartTime: plannedStart,
|
||||
plannedEndTime: plannedEnd,
|
||||
[shift] = await tx.insert(shifts).values({
|
||||
siteId: site.id,
|
||||
startTime: shiftStart,
|
||||
endTime: shiftEnd,
|
||||
shiftType: site.shiftType || "fixed_post",
|
||||
status: "planned",
|
||||
}).returning();
|
||||
|
||||
createdAssignments.push(assignment);
|
||||
}
|
||||
|
||||
return { assignments: createdAssignments, count: createdAssignments.length };
|
||||
// Recheck overlaps within transaction to prevent race conditions
|
||||
const existingAssignments = await tx
|
||||
.select()
|
||||
.from(shiftAssignments)
|
||||
.where(eq(shiftAssignments.guardId, guard.id));
|
||||
|
||||
for (const existing of existingAssignments) {
|
||||
const hasOverlap =
|
||||
plannedStart < existing.plannedEndTime &&
|
||||
plannedEnd > existing.plannedStartTime;
|
||||
|
||||
if (hasOverlap) {
|
||||
throw new Error(
|
||||
`Conflitto: guardia già assegnata ${existing.plannedStartTime.toLocaleString()} - ${existing.plannedEndTime.toLocaleString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create assignment within transaction
|
||||
const [assignment] = await tx.insert(shiftAssignments).values({
|
||||
shiftId: shift.id,
|
||||
guardId: guard.id,
|
||||
plannedStartTime: plannedStart,
|
||||
plannedEndTime: plannedEnd,
|
||||
}).returning();
|
||||
|
||||
return { assignment, shift };
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `Guard assigned successfully for ${result.count} day(s)`,
|
||||
assignments: result.assignments,
|
||||
count: result.count
|
||||
message: "Guard assigned successfully",
|
||||
assignment: result.assignment,
|
||||
shift: result.shift
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error assigning guard:", error);
|
||||
|
||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
||||
{
|
||||
"version": "1.0.26",
|
||||
"lastUpdate": "2025-10-21T15:40:58.930Z",
|
||||
"version": "1.0.25",
|
||||
"lastUpdate": "2025-10-21T14:11:11.154Z",
|
||||
"changelog": [
|
||||
{
|
||||
"version": "1.0.26",
|
||||
"date": "2025-10-21",
|
||||
"type": "patch",
|
||||
"description": "Deployment automatico v1.0.26"
|
||||
},
|
||||
{
|
||||
"version": "1.0.25",
|
||||
"date": "2025-10-21",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user