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
|
localPort = 42175
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 43169
|
|
||||||
externalPort = 5000
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43267
|
localPort = 43267
|
||||||
externalPort = 3003
|
externalPort = 3003
|
||||||
|
|||||||
@ -79,7 +79,6 @@ export default function GeneralPlanning() {
|
|||||||
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
||||||
const [startTime, setStartTime] = useState<string>("06:00");
|
const [startTime, setStartTime] = useState<string>("06:00");
|
||||||
const [durationHours, setDurationHours] = useState<number>(8);
|
const [durationHours, setDurationHours] = useState<number>(8);
|
||||||
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
|
||||||
|
|
||||||
// Query per dati planning settimanale
|
// Query per dati planning settimanale
|
||||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||||
@ -124,9 +123,9 @@ export default function GeneralPlanning() {
|
|||||||
enabled: !!selectedCell, // Query attiva solo se dialog è aperto
|
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({
|
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);
|
return apiRequest("POST", "/api/general-planning/assign-guard", data);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -202,7 +201,6 @@ export default function GeneralPlanning() {
|
|||||||
guardId: selectedGuardId,
|
guardId: selectedGuardId,
|
||||||
startTime,
|
startTime,
|
||||||
durationHours,
|
durationHours,
|
||||||
consecutiveDays,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -495,13 +493,7 @@ export default function GeneralPlanning() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Dialog dettagli cella */}
|
{/* Dialog dettagli cella */}
|
||||||
<Dialog open={!!selectedCell} onOpenChange={() => {
|
<Dialog open={!!selectedCell} onOpenChange={() => setSelectedCell(null)}>
|
||||||
setSelectedCell(null);
|
|
||||||
setSelectedGuardId("");
|
|
||||||
setStartTime("06:00");
|
|
||||||
setDurationHours(8);
|
|
||||||
setConsecutiveDays(1);
|
|
||||||
}}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
@ -593,31 +585,14 @@ export default function GeneralPlanning() {
|
|||||||
|
|
||||||
{/* Form assegnazione guardia */}
|
{/* Form assegnazione guardia */}
|
||||||
<div className="border-t pt-4 space-y-4">
|
<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">
|
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Assegna Nuova Guardia
|
Assegna Guardia
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{/* Ora Inizio, Durata e Giorni */}
|
{/* Ora Inizio e Durata */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="start-time">Ora Inizio</Label>
|
<Label htmlFor="start-time">Ora Inizio</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -642,19 +617,6 @@ export default function GeneralPlanning() {
|
|||||||
data-testid="input-duration"
|
data-testid="input-duration"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Ora fine calcolata */}
|
{/* 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.
205
server/routes.ts
205
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) => {
|
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
|
||||||
try {
|
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) {
|
if (!siteId || !date || !guardId || !startTime || !durationHours) {
|
||||||
return res.status(400).json({
|
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
|
// Get site to check contract and service details
|
||||||
const site = await storage.getSite(siteId);
|
const site = await storage.getSite(siteId);
|
||||||
if (!site) {
|
if (!site) {
|
||||||
@ -1204,124 +1200,113 @@ 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 date and time
|
||||||
const [year, month, day] = date.split("-").map(Number);
|
const shiftDate = parseISO(date);
|
||||||
if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) {
|
if (!isValid(shiftDate)) {
|
||||||
return res.status(400).json({ message: "Invalid date format. Expected YYYY-MM-DD" });
|
return res.status(400).json({ message: "Invalid date format" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate planned start and end times
|
||||||
const [hours, minutes] = startTime.split(":").map(Number);
|
const [hours, minutes] = startTime.split(":").map(Number);
|
||||||
|
const plannedStart = new Date(shiftDate);
|
||||||
|
plannedStart.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
// Atomic transaction: create assignments for all consecutive days
|
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 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
|
let existingShifts = await tx
|
||||||
for (let dayOffset = 0; dayOffset < consecutiveDays; dayOffset++) {
|
.select()
|
||||||
// Calculate date for this iteration
|
.from(shifts)
|
||||||
const currentDate = new Date(year, month - 1, day + dayOffset);
|
.where(
|
||||||
const shiftDate = new Date(currentDate);
|
and(
|
||||||
shiftDate.setHours(0, 0, 0, 0);
|
eq(shifts.siteId, siteId),
|
||||||
|
gte(shifts.startTime, dayStart),
|
||||||
|
lte(shifts.startTime, dayEnd)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Check contract validity for this date
|
let shift;
|
||||||
if (site.contractStartDate && site.contractEndDate) {
|
if (existingShifts.length > 0) {
|
||||||
const contractStart = new Date(site.contractStartDate);
|
shift = existingShifts[0];
|
||||||
const contractEnd = new Date(site.contractEndDate);
|
} else {
|
||||||
if (shiftDate < contractStart || shiftDate > contractEnd) {
|
// Create new shift for full service period
|
||||||
throw new Error(
|
const serviceStart = site.serviceStartTime || "00:00";
|
||||||
`Cannot assign guard for date ${shiftDate.toLocaleDateString()}: outside contract period`
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate planned start and end times for this day
|
[shift] = await tx.insert(shifts).values({
|
||||||
const plannedStart = new Date(currentDate);
|
siteId: site.id,
|
||||||
plannedStart.setHours(hours, minutes, 0, 0);
|
startTime: shiftStart,
|
||||||
const plannedEnd = new Date(currentDate);
|
endTime: shiftEnd,
|
||||||
plannedEnd.setHours(hours + durationHours, minutes, 0, 0);
|
shiftType: site.shiftType || "fixed_post",
|
||||||
|
status: "planned",
|
||||||
// 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,
|
|
||||||
}).returning();
|
}).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({
|
res.json({
|
||||||
message: `Guard assigned successfully for ${result.count} day(s)`,
|
message: "Guard assigned successfully",
|
||||||
assignments: result.assignments,
|
assignment: result.assignment,
|
||||||
count: result.count
|
shift: result.shift
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error assigning guard:", error);
|
console.error("Error assigning guard:", error);
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.26",
|
"version": "1.0.25",
|
||||||
"lastUpdate": "2025-10-21T15:40:58.930Z",
|
"lastUpdate": "2025-10-21T14:11:11.154Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
{
|
|
||||||
"version": "1.0.26",
|
|
||||||
"date": "2025-10-21",
|
|
||||||
"type": "patch",
|
|
||||||
"description": "Deployment automatico v1.0.26"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "1.0.25",
|
"version": "1.0.25",
|
||||||
"date": "2025-10-21",
|
"date": "2025-10-21",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user