Compare commits

..

No commits in common. "6b6db9474ec1de0e5a89a037679272c7a888d3ff" and "2616fb775a51c6d2e31799b6dc7fb09e22e3113d" have entirely different histories.

7 changed files with 51 additions and 64 deletions

Binary file not shown.

Before

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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2 } from "lucide-react"; import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
@ -280,26 +280,8 @@ export default function GeneralPlanning() {
data-testid={`cell-${site.siteId}-${day.date}`} data-testid={`cell-${site.siteId}-${day.date}`}
onClick={() => daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)} onClick={() => daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)}
> >
{daySiteData ? ( {daySiteData && daySiteData.shiftsCount > 0 ? (
<div className="space-y-2 text-xs"> <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 */} {/* Guardie assegnate */}
{daySiteData.guards.length > 0 && ( {daySiteData.guards.length > 0 && (
<div className="space-y-1"> <div className="space-y-1">
@ -333,13 +315,21 @@ export default function GeneralPlanning() {
</div> </div>
)} )}
{/* Info copertura - mostra solo se ci sono turni */} {/* Guardie mancanti */}
{daySiteData.shiftsCount > 0 && ( {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>
</div>
)}
{/* Info copertura */}
<div className="text-xs text-muted-foreground pt-1 border-t"> <div className="text-xs text-muted-foreground pt-1 border-t">
<div>Turni: {daySiteData.shiftsCount}</div> <div>Turni: {daySiteData.shiftsCount}</div>
<div>Tot. ore: {daySiteData.totalShiftHours}h</div> <div>Tot. ore: {daySiteData.totalShiftHours}h</div>
</div> </div>
)}
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">

View File

@ -956,6 +956,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
.filter(Boolean); .filter(Boolean);
// Calcolo guardie mancanti // Calcolo guardie mancanti
// Formula: ceil(24 / maxOreGuardia) × minGuardie - guardieAssegnate
const maxOreGuardia = 9; // Max ore per guardia const maxOreGuardia = 9; // Max ore per guardia
const minGuardie = site.minGuards || 1; const minGuardie = site.minGuards || 1;
@ -966,25 +967,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
return sum + differenceInHours(end, start); return sum + differenceInHours(end, start);
}, 0); }, 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) // Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size; const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size;
// Calcolo guardie necessarie e mancanti // Guardie mancanti
let totalGuardsNeeded: number; const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
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 { return {
siteId: site.id, siteId: site.id,

View File

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

View File

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