Compare commits
2 Commits
62f8189e7d
...
dd7adeaa24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7adeaa24 | ||
|
|
100f20e422 |
4
.replit
4
.replit
@ -31,6 +31,10 @@ externalPort = 4200
|
||||
localPort = 42175
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 43169
|
||||
externalPort = 5000
|
||||
|
||||
[[ports]]
|
||||
localPort = 43267
|
||||
externalPort = 3003
|
||||
|
||||
@ -79,6 +79,7 @@ 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>({
|
||||
@ -123,9 +124,9 @@ export default function GeneralPlanning() {
|
||||
enabled: !!selectedCell, // Query attiva solo se dialog è aperto
|
||||
});
|
||||
|
||||
// Mutation per assegnare guardia con orari
|
||||
// Mutation per assegnare guardia con orari (anche multi-giorno)
|
||||
const assignGuardMutation = useMutation({
|
||||
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number }) => {
|
||||
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number }) => {
|
||||
return apiRequest("POST", "/api/general-planning/assign-guard", data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
@ -201,6 +202,7 @@ export default function GeneralPlanning() {
|
||||
guardId: selectedGuardId,
|
||||
startTime,
|
||||
durationHours,
|
||||
consecutiveDays,
|
||||
});
|
||||
};
|
||||
|
||||
@ -493,7 +495,13 @@ export default function GeneralPlanning() {
|
||||
</Card>
|
||||
|
||||
{/* Dialog dettagli cella */}
|
||||
<Dialog open={!!selectedCell} onOpenChange={() => setSelectedCell(null)}>
|
||||
<Dialog open={!!selectedCell} onOpenChange={() => {
|
||||
setSelectedCell(null);
|
||||
setSelectedGuardId("");
|
||||
setStartTime("06:00");
|
||||
setDurationHours(8);
|
||||
setConsecutiveDays(1);
|
||||
}}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
@ -585,14 +593,31 @@ 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 Guardia
|
||||
Assegna Nuova Guardia
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{/* Ora Inizio e Durata */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Ora Inizio, Durata e Giorni */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="start-time">Ora Inizio</Label>
|
||||
<Input
|
||||
@ -617,6 +642,19 @@ 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 */}
|
||||
|
||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.26_20251021_154042.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.26_20251021_154042.sql.gz
Normal file
Binary file not shown.
@ -1177,10 +1177,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Assign guard to site/date with specific time slot
|
||||
// Assign guard to site/date with specific time slot (supports multi-day assignments)
|
||||
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { siteId, date, guardId, startTime, durationHours } = req.body;
|
||||
const { siteId, date, guardId, startTime, durationHours, consecutiveDays = 1 } = req.body;
|
||||
|
||||
if (!siteId || !date || !guardId || !startTime || !durationHours) {
|
||||
return res.status(400).json({
|
||||
@ -1188,6 +1188,10 @@ 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) {
|
||||
@ -1200,33 +1204,41 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
return res.status(404).json({ message: "Guard not found" });
|
||||
}
|
||||
|
||||
// Parse date and time
|
||||
const shiftDate = parseISO(date);
|
||||
if (!isValid(shiftDate)) {
|
||||
return res.status(400).json({ message: "Invalid date format" });
|
||||
// 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" });
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Atomic transaction: create assignments for all consecutive days
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const createdAssignments = [];
|
||||
|
||||
// Check contract validity
|
||||
// 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
|
||||
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`
|
||||
});
|
||||
throw new Error(
|
||||
`Cannot assign guard for date ${shiftDate.toLocaleDateString()}: outside contract period`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic transaction: find/create shift + verify no overlaps + create assignment
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// 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);
|
||||
@ -1274,7 +1286,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}).returning();
|
||||
}
|
||||
|
||||
// Recheck overlaps within transaction to prevent race conditions
|
||||
// Recheck overlaps within transaction for this day
|
||||
const existingAssignments = await tx
|
||||
.select()
|
||||
.from(shiftAssignments)
|
||||
@ -1292,7 +1304,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
}
|
||||
|
||||
// Create assignment within transaction
|
||||
// Create assignment for this day
|
||||
const [assignment] = await tx.insert(shiftAssignments).values({
|
||||
shiftId: shift.id,
|
||||
guardId: guard.id,
|
||||
@ -1300,13 +1312,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
plannedEndTime: plannedEnd,
|
||||
}).returning();
|
||||
|
||||
return { assignment, shift };
|
||||
createdAssignments.push(assignment);
|
||||
}
|
||||
|
||||
return { assignments: createdAssignments, count: createdAssignments.length };
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: "Guard assigned successfully",
|
||||
assignment: result.assignment,
|
||||
shift: result.shift
|
||||
message: `Guard assigned successfully for ${result.count} day(s)`,
|
||||
assignments: result.assignments,
|
||||
count: result.count
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error assigning guard:", error);
|
||||
|
||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
||||
{
|
||||
"version": "1.0.25",
|
||||
"lastUpdate": "2025-10-21T14:11:11.154Z",
|
||||
"version": "1.0.26",
|
||||
"lastUpdate": "2025-10-21T15:40:58.930Z",
|
||||
"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