Compare commits

..

No commits in common. "dd7adeaa2413f5a4442507a7e5688cc844b2dd84" and "62f8189e7dbe116868e447dbf29004e1cc25f455" have entirely different histories.

6 changed files with 104 additions and 167 deletions

View File

@ -31,10 +31,6 @@ externalPort = 4200
localPort = 42175
externalPort = 3002
[[ports]]
localPort = 43169
externalPort = 5000
[[ports]]
localPort = 43267
externalPort = 3003

View File

@ -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 */}

View File

@ -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,41 +1200,33 @@ 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" });
}
// Calculate planned start and end times
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 result = await db.transaction(async (tx) => {
const createdAssignments = [];
const plannedEnd = new Date(plannedStart);
plannedEnd.setHours(plannedStart.getHours() + durationHours);
// 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);
// Check contract validity for this date
// 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) {
throw new Error(
`Cannot assign guard for date ${shiftDate.toLocaleDateString()}: outside contract period`
);
return res.status(400).json({
message: `Cannot assign guard: date outside contract period`
});
}
}
// 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);
// Atomic transaction: find/create shift + verify no overlaps + create assignment
const result = await db.transaction(async (tx) => {
// Find or create shift for this site/date
const dayStart = new Date(shiftDate);
dayStart.setHours(0, 0, 0, 0);
@ -1286,7 +1274,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
}).returning();
}
// Recheck overlaps within transaction for this day
// Recheck overlaps within transaction to prevent race conditions
const existingAssignments = await tx
.select()
.from(shiftAssignments)
@ -1304,7 +1292,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
}
// Create assignment for this day
// Create assignment within transaction
const [assignment] = await tx.insert(shiftAssignments).values({
shiftId: shift.id,
guardId: guard.id,
@ -1312,16 +1300,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
plannedEndTime: plannedEnd,
}).returning();
createdAssignments.push(assignment);
}
return { assignments: createdAssignments, count: createdAssignments.length };
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);

View File

@ -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",