Compare commits

..

2 Commits

Author SHA1 Message Date
Marco Lanzara
6b6db9474e 🚀 Release v1.0.21
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.21_20251018_081811.sql.gz
- Data: 2025-10-18 08:18:27
2025-10-18 08:18:27 +00:00
marco370
b1e5a13882 Update planning view to show guard coverage and calculation status
Add new UI elements to display guard coverage status, including total needed, assigned, and missing guards, and update the display logic to accurately reflect these calculations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/uZXH8P1
2025-10-18 07:46:25 +00:00
7 changed files with 64 additions and 51 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -6,7 +6,7 @@ import { useLocation } from "wouter";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit } from "lucide-react";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
@ -280,8 +280,26 @@ export default function GeneralPlanning() {
data-testid={`cell-${site.siteId}-${day.date}`}
onClick={() => daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)}
>
{daySiteData && daySiteData.shiftsCount > 0 ? (
{daySiteData ? (
<div className="space-y-2 text-xs">
{/* Riepilogo guardie necessarie/assegnate/mancanti - SEMPRE VISIBILE */}
<div className="pb-2 border-b">
{daySiteData.missingGuards > 0 ? (
<Badge variant="destructive" className="w-full justify-center gap-1">
<AlertTriangle className="h-3 w-3" />
Mancano {daySiteData.missingGuards} {daySiteData.missingGuards === 1 ? "guardia" : "guardie"}
</Badge>
) : (
<Badge variant="default" className="w-full justify-center gap-1 bg-green-600 hover:bg-green-700">
<CheckCircle2 className="h-3 w-3" />
Copertura Completa
</Badge>
)}
<div className="text-xs text-muted-foreground mt-1 text-center">
{daySiteData.guardsAssigned + daySiteData.missingGuards} necessarie · {daySiteData.guardsAssigned} assegnate
</div>
</div>
{/* Guardie assegnate */}
{daySiteData.guards.length > 0 && (
<div className="space-y-1">
@ -315,21 +333,13 @@ export default function GeneralPlanning() {
</div>
)}
{/* Guardie mancanti */}
{daySiteData.missingGuards > 0 && (
<div className="pt-2 border-t">
<Badge variant="destructive" className="w-full justify-center gap-1">
<AlertTriangle className="h-3 w-3" />
Mancano {daySiteData.missingGuards} {daySiteData.missingGuards === 1 ? "guardia" : "guardie"}
</Badge>
{/* Info copertura - mostra solo se ci sono turni */}
{daySiteData.shiftsCount > 0 && (
<div className="text-xs text-muted-foreground pt-1 border-t">
<div>Turni: {daySiteData.shiftsCount}</div>
<div>Tot. ore: {daySiteData.totalShiftHours}h</div>
</div>
)}
{/* Info copertura */}
<div className="text-xs text-muted-foreground pt-1 border-t">
<div>Turni: {daySiteData.shiftsCount}</div>
<div>Tot. ore: {daySiteData.totalShiftHours}h</div>
</div>
</div>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">

View File

@ -956,7 +956,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
.filter(Boolean);
// Calcolo guardie mancanti
// Formula: ceil(24 / maxOreGuardia) × minGuardie - guardieAssegnate
const maxOreGuardia = 9; // Max ore per guardia
const minGuardie = site.minGuards || 1;
@ -967,17 +966,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
return sum + differenceInHours(end, start);
}, 0);
// Slot necessari per coprire le ore totali
const slotsNeeded = totalShiftHours > 0 ? Math.ceil(totalShiftHours / maxOreGuardia) : 0;
// Guardie totali necessarie (slot × min guardie contemporanee)
const totalGuardsNeeded = slotsNeeded * minGuardie;
// Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size;
// Guardie mancanti
const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
// Calcolo guardie necessarie e mancanti
let totalGuardsNeeded: number;
let missingGuards: number;
if (totalShiftHours > 0) {
// Se ci sono turni: calcola basandosi sulle ore
// Slot necessari per coprire le ore totali
const slotsNeeded = Math.ceil(totalShiftHours / maxOreGuardia);
// Guardie totali necessarie (slot × min guardie contemporanee)
totalGuardsNeeded = slotsNeeded * minGuardie;
missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
} else {
// Se NON ci sono turni: serve almeno la copertura minima
totalGuardsNeeded = minGuardie;
missingGuards = minGuardie; // Tutte mancanti perché non ci sono turni
}
return {
siteId: site.id,

View File

@ -163,32 +163,22 @@ export class DatabaseStorage implements IStorage {
}
async upsertUser(userData: UpsertUser): Promise<User> {
// Check if user already exists by email (unique constraint)
const existingUser = await db
.select()
.from(users)
.where(eq(users.email, userData.email || ''))
.limit(1);
if (existingUser.length > 0) {
// Update existing user
const [updated] = await db
.update(users)
.set({
...userData,
// Use onConflictDoUpdate to handle both insert and update cases
// This handles conflicts on both id (primary key) and email (unique constraint)
const [user] = await db
.insert(users)
.values(userData)
.onConflictDoUpdate({
target: users.id,
set: {
email: userData.email,
name: userData.name,
role: userData.role,
updatedAt: new Date(),
})
.where(eq(users.email, userData.email || ''))
.returning();
return updated;
} else {
// Insert new user
const [user] = await db
.insert(users)
.values(userData)
.returning();
return user;
}
},
})
.returning();
return user;
}
async getAllUsers(): Promise<User[]> {

View File

@ -1,7 +1,13 @@
{
"version": "1.0.20",
"lastUpdate": "2025-10-18T07:41:49.922Z",
"version": "1.0.21",
"lastUpdate": "2025-10-18T08:18:27.659Z",
"changelog": [
{
"version": "1.0.21",
"date": "2025-10-18",
"type": "patch",
"description": "Deployment automatico v1.0.21"
},
{
"version": "1.0.20",
"date": "2025-10-18",