Compare commits

..

No commits in common. "cd986635e4a67a3ff5e36f9a41e8d928feb216f8" and "4693b782cb64b9e5963992771f283cb2e8f5d320" have entirely different histories.

10 changed files with 99 additions and 306 deletions

View File

@ -28,7 +28,7 @@ localPort = 42175
externalPort = 3002
[[ports]]
localPort = 43267
localPort = 42403
externalPort = 3003
[env]

View File

@ -3,7 +3,7 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertSiteSchema, insertServiceTypeSchema, type Site, type ServiceType } from "@shared/schema";
import { insertSiteSchema, type Site } from "@shared/schema";
import { z } from "zod";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@ -35,27 +35,34 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import * as LucideIcons from "lucide-react";
import { MapPin, Plus, Pencil } from "lucide-react";
import { Building2, Shield, Eye, MapPin, Zap, Plus, Pencil } from "lucide-react";
// Helper to get icon component from name
const getIconComponent = (iconName: string) => {
const Icon = (LucideIcons as any)[iconName];
return Icon || LucideIcons.Building2;
};
// Helper to get color classes from color name
const getColorClasses = (color: string) => {
const colorMap: Record<string, string> = {
blue: "bg-blue-500/10 text-blue-500 border-blue-500/20",
green: "bg-green-500/10 text-green-500 border-green-500/20",
purple: "bg-purple-500/10 text-purple-500 border-purple-500/20",
orange: "bg-orange-500/10 text-orange-500 border-orange-500/20",
red: "bg-red-500/10 text-red-500 border-red-500/20",
yellow: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
};
return colorMap[color] || colorMap.blue;
};
const serviceTypeInfo = {
fixed_post: {
label: "Presidio Fisso",
description: "Guardia fissa presso una struttura",
icon: Building2,
color: "bg-blue-500/10 text-blue-500 border-blue-500/20"
},
patrol: {
label: "Pattugliamento",
description: "Ronde e controlli su area",
icon: Eye,
color: "bg-green-500/10 text-green-500 border-green-500/20"
},
night_inspection: {
label: "Ispettorato Notturno",
description: "Controlli notturni programmati",
icon: Shield,
color: "bg-purple-500/10 text-purple-500 border-purple-500/20"
},
quick_response: {
label: "Pronto Intervento",
description: "Intervento rapido su chiamata",
icon: Zap,
color: "bg-orange-500/10 text-orange-500 border-orange-500/20"
}
} as const;
type SiteForm = z.infer<typeof insertSiteSchema>;
@ -66,16 +73,10 @@ export default function Services() {
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const { data: sites = [], isLoading: isLoadingSites } = useQuery<Site[]>({
const { data: sites = [], isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
const { data: serviceTypes = [], isLoading: isLoadingServiceTypes } = useQuery<ServiceType[]>({
queryKey: ["/api/service-types"],
});
const isLoading = isLoadingSites || isLoadingServiceTypes;
const createForm = useForm<SiteForm>({
resolver: zodResolver(insertSiteSchema),
defaultValues: {
@ -182,9 +183,9 @@ export default function Services() {
};
// Calculate statistics per service type
const stats = serviceTypes.reduce((acc, serviceType) => {
const sitesForType = sites.filter(s => s.shiftType === serviceType.code);
acc[serviceType.code] = {
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
const sitesForType = sites.filter(s => s.shiftType === type);
acc[type] = {
total: sitesForType.length,
active: sitesForType.filter(s => s.isActive).length,
requiresArmed: sitesForType.filter(s => s.requiresArmed).length,
@ -213,48 +214,56 @@ export default function Services() {
) : (
<>
<div className="grid gap-6 md:grid-cols-2">
{serviceTypes.map((serviceType) => {
const Icon = getIconComponent(serviceType.icon);
const stat = stats[serviceType.code] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
{Object.entries(serviceTypeInfo).map(([type, info]) => {
const Icon = info.icon;
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
return (
<Card key={serviceType.id} data-testid={`card-service-${serviceType.code}`}>
<Card key={type} data-testid={`card-service-${type}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${getColorClasses(serviceType.color)}`}>
<div className={`p-2 rounded-lg ${info.color}`}>
<Icon className="h-6 w-6" />
</div>
<div>
<CardTitle className="text-xl">{serviceType.label}</CardTitle>
<CardDescription className="mt-1">{serviceType.description}</CardDescription>
<CardTitle className="text-xl">{info.label}</CardTitle>
<CardDescription className="mt-1">{info.description}</CardDescription>
</div>
</div>
<Button
size="sm"
onClick={() => handleCreateSite(type)}
data-testid={`button-add-site-${type}`}
>
<Plus className="h-4 w-4 mr-1" />
Aggiungi Sito
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Siti Totali</p>
<p className="text-2xl font-semibold" data-testid={`text-total-${serviceType.code}`}>
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}>
{stat.total}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Attivi</p>
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${serviceType.code}`}>
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
{stat.active}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Armati</p>
<p className="text-lg font-semibold" data-testid={`text-armed-${serviceType.code}`}>
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}>
{stat.requiresArmed}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Patente</p>
<p className="text-lg font-semibold" data-testid={`text-driver-${serviceType.code}`}>
<p className="text-lg font-semibold" data-testid={`text-driver-${type}`}>
{stat.requiresDriver}
</p>
</div>
@ -264,15 +273,15 @@ export default function Services() {
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === serviceType.code && s.location === 'roccapiemonte').length} Roccapiemonte
{sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === serviceType.code && s.location === 'milano').length} Milano
{sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === serviceType.code && s.location === 'roma').length} Roma
{sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma
</Badge>
</div>
</div>
@ -282,10 +291,10 @@ export default function Services() {
variant="ghost"
size="sm"
className="w-full mt-4"
onClick={() => setSelectedServiceType(selectedServiceType === serviceType.code ? null : serviceType.code)}
data-testid={`button-view-sites-${serviceType.code}`}
onClick={() => setSelectedServiceType(selectedServiceType === type ? null : type)}
data-testid={`button-view-sites-${type}`}
>
{selectedServiceType === serviceType.code ? "Nascondi" : "Visualizza"} Siti ({stat.total})
{selectedServiceType === type ? "Nascondi" : "Visualizza"} Siti ({stat.total})
</Button>
)}
</CardContent>
@ -298,7 +307,7 @@ export default function Services() {
<Card>
<CardHeader>
<CardTitle>
Siti {serviceTypes.find(st => st.code === selectedServiceType)?.label || ""}
Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label}
</CardTitle>
<CardDescription>
Lista completa dei siti con questo tipo di servizio
@ -420,9 +429,10 @@ export default function Services() {
</SelectTrigger>
</FormControl>
<SelectContent>
{serviceTypes.filter(st => st.isActive).map(st => (
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
))}
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@ -606,9 +616,10 @@ export default function Services() {
</SelectTrigger>
</FormControl>
<SelectContent>
{serviceTypes.filter(st => st.isActive).map(st => (
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
))}
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
</SelectContent>
</Select>
<FormMessage />

View File

@ -511,18 +511,14 @@ export default function Vehicles() {
render={({ field }) => (
<FormItem>
<FormLabel>Assegnato a</FormLabel>
<Select
onValueChange={(value) => field.onChange(value === "none" ? null : value)}
value={field.value || "none"}
data-testid="select-create-guard"
>
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-create-guard">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">Nessuno</SelectItem>
<SelectItem value="">Nessuno</SelectItem>
{guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber}
@ -742,18 +738,14 @@ export default function Vehicles() {
render={({ field }) => (
<FormItem>
<FormLabel>Assegnato a</FormLabel>
<Select
onValueChange={(value) => field.onChange(value === "none" ? null : value)}
value={field.value || "none"}
data-testid="select-edit-guard"
>
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-edit-guard">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">Nessuno</SelectItem>
<SelectItem value="">Nessuno</SelectItem>
{guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber}

View File

@ -135,8 +135,7 @@ else
log_info "Creo backup: $BACKUP_FILE"
# Backup con pg_dump (include schema e dati)
# Opzioni: --clean (DROP before CREATE), --if-exists (no error if not exists), --inserts (INSERT statements per compatibilità)
if pg_dump "$DATABASE_URL" --clean --if-exists --inserts > "$BACKUP_FILE"; then
if pg_dump "$DATABASE_URL" > "$BACKUP_FILE" 2>/dev/null; then
# Comprimi backup
gzip "$BACKUP_FILE"
BACKUP_FILE="${BACKUP_FILE}.gz"
@ -148,7 +147,7 @@ else
log_info "Pulizia backup vecchi (mantengo ultimi 10)..."
ls -t $BACKUP_DIR/*.sql.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true
else
log_error "Backup database fallito! Verifica DATABASE_URL e permessi pg_dump"
log_error "Backup database fallito!"
exit 1
fi
fi

View File

@ -2,21 +2,11 @@
set -e
# Script di deployment automatico per VigilanzaTurni
# Uso:
# bash deploy/deploy.sh → deployment normale
# bash deploy/deploy.sh db → deployment + importa backup database da Git
# Uso: bash deploy/deploy.sh
APP_DIR="/var/www/vigilanza-turni"
APP_NAME="vigilanza-turni"
BACKUP_DIR="/var/backups/vigilanza-turni"
GIT_BACKUP_DIR="$APP_DIR/database-backups"
# Verifica parametro per import database
IMPORT_DB=false
if [ "$1" == "db" ]; then
IMPORT_DB=true
echo "🔄 Modalità: Deployment con import database da Git"
fi
echo "🚀 Deployment VigilanzaTurni - $(date)"
@ -29,52 +19,16 @@ if [ -d .git ]; then
git pull origin main || true
fi
# =================== BACKUP DATABASE ===================
echo "💾 Backup database pre-deployment..."
mkdir -p $BACKUP_DIR
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
# Load env vars
if [ -f .env ]; then
source .env
fi
# =================== IMPORT DATABASE DA GIT (se richiesto) ===================
if [ "$IMPORT_DB" = true ]; then
echo "📦 Import database da backup Git..."
# Trova l'ultimo backup nella directory git
LATEST_GIT_BACKUP=$(ls -t $GIT_BACKUP_DIR/*.sql.gz 2>/dev/null | head -1)
if [ -f "$LATEST_GIT_BACKUP" ]; then
echo "📥 Trovato backup: $LATEST_GIT_BACKUP"
# Controlla che le variabili siano definite
if [ -z "$PGDATABASE" ] || [ -z "$PGUSER" ]; then
echo "❌ Variabili DB non trovate nel .env, impossibile importare"
exit 1
fi
# Chiedi conferma (opzionale - rimuovi se vuoi import automatico)
echo "⚠️ ATTENZIONE: Questo sovrascriverà il database corrente!"
echo "Backup da importare: $(basename $LATEST_GIT_BACKUP)"
read -p "Continuare? (s/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Ss]$ ]]; then
# Decomprimi e importa
echo "🔄 Importazione database in corso..."
gunzip -c "$LATEST_GIT_BACKUP" | PGPASSWORD=$PGPASSWORD psql -h ${PGHOST:-localhost} -U $PGUSER --dbname=$PGDATABASE
echo "✅ Database importato con successo da Git!"
else
echo "❌ Import annullato dall'utente"
exit 1
fi
else
echo "❌ Nessun backup trovato in $GIT_BACKUP_DIR"
exit 1
fi
else
# =================== BACKUP DATABASE NORMALE ===================
echo "💾 Backup database pre-deployment..."
mkdir -p $BACKUP_DIR
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
# Esegui backup PostgreSQL
if command -v pg_dump &> /dev/null; then
# Controlla che le variabili siano definite
@ -95,12 +49,11 @@ else
else
echo "⚠️ pg_dump non trovato, skip backup"
fi
fi
# =================== BUILD & DEPLOY ===================
# Installa TUTTE le dipendenze (serve per build e migrations)
echo "📥 Installazione dipendenze (include devDependencies)..."
npm ci --include=dev
npm ci
# Build frontend
echo "🏗️ Build frontend Vite..."

View File

@ -418,53 +418,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// ============= SERVICE TYPE ROUTES =============
app.get("/api/service-types", isAuthenticated, async (req, res) => {
try {
const serviceTypes = await storage.getAllServiceTypes();
res.json(serviceTypes);
} catch (error) {
console.error("Error fetching service types:", error);
res.status(500).json({ message: "Failed to fetch service types" });
}
});
app.post("/api/service-types", isAuthenticated, async (req, res) => {
try {
const serviceType = await storage.createServiceType(req.body);
res.json(serviceType);
} catch (error) {
console.error("Error creating service type:", error);
res.status(500).json({ message: "Failed to create service type" });
}
});
app.patch("/api/service-types/:id", isAuthenticated, async (req, res) => {
try {
const serviceType = await storage.updateServiceType(req.params.id, req.body);
if (!serviceType) {
return res.status(404).json({ message: "Service type not found" });
}
res.json(serviceType);
} catch (error) {
console.error("Error updating service type:", error);
res.status(500).json({ message: "Failed to update service type" });
}
});
app.delete("/api/service-types/:id", isAuthenticated, async (req, res) => {
try {
const serviceType = await storage.deleteServiceType(req.params.id);
if (!serviceType) {
return res.status(404).json({ message: "Service type not found" });
}
res.json(serviceType);
} catch (error) {
console.error("Error deleting service type:", error);
res.status(500).json({ message: "Failed to delete service type" });
}
});
// ============= SITE ROUTES =============
app.get("/api/sites", isAuthenticated, async (req, res) => {
try {

View File

@ -1,58 +1,11 @@
import { db } from "./db";
import { users, guards, sites, vehicles, contractParameters, serviceTypes } from "@shared/schema";
import { users, guards, sites, vehicles, contractParameters } from "@shared/schema";
import { eq } from "drizzle-orm";
import bcrypt from "bcrypt";
async function seed() {
console.log("🌱 Avvio seed database multi-sede...");
// Create Service Types
console.log("📝 Creazione tipologie di servizi...");
const defaultServiceTypes = [
{
code: "fixed_post",
label: "Presidio Fisso",
description: "Guardia fissa presso una struttura",
icon: "Building2",
color: "blue",
isActive: true
},
{
code: "patrol",
label: "Pattugliamento",
description: "Ronde e controlli su area",
icon: "Eye",
color: "green",
isActive: true
},
{
code: "night_inspection",
label: "Ispettorato Notturno",
description: "Controlli notturni programmati",
icon: "Shield",
color: "purple",
isActive: true
},
{
code: "quick_response",
label: "Pronto Intervento",
description: "Intervento rapido su chiamata",
icon: "Zap",
color: "orange",
isActive: true
}
];
for (const serviceType of defaultServiceTypes) {
const existing = await db.select().from(serviceTypes).where(eq(serviceTypes.code, serviceType.code)).limit(1);
if (existing.length === 0) {
await db.insert(serviceTypes).values(serviceType);
console.log(` + Creata tipologia: ${serviceType.label}`);
} else {
console.log(` ✓ Tipologia esistente: ${serviceType.label}`);
}
}
// Create CCNL contract parameters
console.log("📋 Creazione parametri contrattuali CCNL...");
const existingParams = await db.select().from(contractParameters).limit(1);

View File

@ -16,7 +16,6 @@ import {
absences,
absenceAffectedShifts,
contractParameters,
serviceTypes,
type User,
type UpsertUser,
type Guard,
@ -49,8 +48,6 @@ import {
type InsertAbsenceAffectedShift,
type ContractParameters,
type InsertContractParameters,
type ServiceType,
type InsertServiceType,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc } from "drizzle-orm";
@ -73,13 +70,6 @@ export interface IStorage {
createCertification(cert: InsertCertification): Promise<Certification>;
updateCertificationStatus(id: string, status: "valid" | "expiring_soon" | "expired"): Promise<void>;
// Service Type operations
getAllServiceTypes(): Promise<ServiceType[]>;
getServiceType(id: string): Promise<ServiceType | undefined>;
createServiceType(serviceType: InsertServiceType): Promise<ServiceType>;
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
deleteServiceType(id: string): Promise<ServiceType | undefined>;
// Site operations
getAllSites(): Promise<Site[]>;
getSite(id: string): Promise<Site | undefined>;
@ -282,35 +272,6 @@ export class DatabaseStorage implements IStorage {
.where(eq(certifications.id, id));
}
// Service Type operations
async getAllServiceTypes(): Promise<ServiceType[]> {
return await db.select().from(serviceTypes).orderBy(desc(serviceTypes.createdAt));
}
async getServiceType(id: string): Promise<ServiceType | undefined> {
const [serviceType] = await db.select().from(serviceTypes).where(eq(serviceTypes.id, id));
return serviceType;
}
async createServiceType(serviceType: InsertServiceType): Promise<ServiceType> {
const [newServiceType] = await db.insert(serviceTypes).values(serviceType).returning();
return newServiceType;
}
async updateServiceType(id: string, serviceTypeData: Partial<InsertServiceType>): Promise<ServiceType | undefined> {
const [updated] = await db
.update(serviceTypes)
.set({ ...serviceTypeData, updatedAt: new Date() })
.where(eq(serviceTypes.id, id))
.returning();
return updated;
}
async deleteServiceType(id: string): Promise<ServiceType | undefined> {
const [deleted] = await db.delete(serviceTypes).where(eq(serviceTypes.id, id)).returning();
return deleted;
}
// Site operations
async getAllSites(): Promise<Site[]> {
return await db.select().from(sites);

View File

@ -174,20 +174,6 @@ export const vehicles = pgTable("vehicles", {
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SERVICE TYPES =============
export const serviceTypes = pgTable("service_types", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
code: varchar("code").notNull().unique(), // fixed_post, patrol, etc.
label: varchar("label").notNull(), // Presidio Fisso, Pattugliamento, etc.
description: text("description"), // Descrizione dettagliata
icon: varchar("icon").notNull().default("Building2"), // Nome icona Lucide
color: varchar("color").notNull().default("blue"), // blue, green, purple, orange
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SITES & CONTRACTS =============
export const sites = pgTable("sites", {
@ -623,12 +609,6 @@ export const insertVehicleSchema = createInsertSchema(vehicles).omit({
updatedAt: true,
});
export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertSiteSchema = createInsertSchema(sites).omit({
id: true,
createdAt: true,
@ -719,9 +699,6 @@ export type Certification = typeof certifications.$inferSelect;
export type InsertVehicle = z.infer<typeof insertVehicleSchema>;
export type Vehicle = typeof vehicles.$inferSelect;
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
export type ServiceType = typeof serviceTypes.$inferSelect;
export type InsertSite = z.infer<typeof insertSiteSchema>;
export type Site = typeof sites.$inferSelect;

View File

@ -1,13 +1,7 @@
{
"version": "1.0.9",
"lastUpdate": "2025-10-17T09:18:14.391Z",
"version": "1.0.8",
"lastUpdate": "2025-10-17T08:36:23.963Z",
"changelog": [
{
"version": "1.0.9",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.9"
},
{
"version": "1.0.8",
"date": "2025-10-17",