Compare commits
2 Commits
2616fb775a
...
6b6db9474e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6db9474e | ||
|
|
b1e5a13882 |
BIN
attached_assets/immagine_1760773435933.png
Normal file
BIN
attached_assets/immagine_1760773435933.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@ -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 } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2 } 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,8 +280,26 @@ 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.shiftsCount > 0 ? (
|
{daySiteData ? (
|
||||||
<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">
|
||||||
@ -315,21 +333,13 @@ export default function GeneralPlanning() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Guardie mancanti */}
|
{/* Info copertura - mostra solo se ci sono turni */}
|
||||||
{daySiteData.missingGuards > 0 && (
|
{daySiteData.shiftsCount > 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">
|
||||||
|
|||||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.21_20251018_081811.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.21_20251018_081811.sql.gz
Normal file
Binary file not shown.
@ -956,7 +956,6 @@ 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;
|
||||||
|
|
||||||
@ -967,17 +966,25 @@ 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;
|
||||||
|
|
||||||
// Guardie mancanti
|
// Calcolo guardie necessarie e mancanti
|
||||||
const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
|
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 {
|
return {
|
||||||
siteId: site.id,
|
siteId: site.id,
|
||||||
|
|||||||
@ -163,33 +163,23 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
async upsertUser(userData: UpsertUser): Promise<User> {
|
||||||
// Check if user already exists by email (unique constraint)
|
// Use onConflictDoUpdate to handle both insert and update cases
|
||||||
const existingUser = await db
|
// This handles conflicts on both id (primary key) and email (unique constraint)
|
||||||
.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));
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.20",
|
"version": "1.0.21",
|
||||||
"lastUpdate": "2025-10-18T07:41:49.922Z",
|
"lastUpdate": "2025-10-18T08:18:27.659Z",
|
||||||
"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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user