Compare commits

...

10 Commits

Author SHA1 Message Date
Marco Lanzara
cd986635e4 🚀 Release v1.0.9
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.9_20251017_091758.sql.gz
- Data: 2025-10-17 09:18:14
2025-10-17 09:18:14 +00:00
marco370
a5355ed881 Add system to manage different types of security services and their details
Introduce new "serviceTypes" table and CRUD operations in the backend. Update frontend to fetch and display service types dynamically. Modify vehicle assignment logic to handle null guards.

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/IgrJ2ut
2025-10-17 09:12:29 +00:00
marco370
204717cd9d Add option to import database backup from Git during deployment
Modify the deployment script to allow importing the latest database backup from the Git repository when the 'db' argument is provided. This includes handling backup decompression and database restoration.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
2025-10-17 08:52:00 +00:00
marco370
ab7b3c7f90 Update script for triggering GitLab push actions automatically
The `push-gitlab.sh` script was updated to automatically trigger GitLab push actions during export operations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 94491beb-cc63-4877-b64a-41f1e2a4c43a
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-17 08:46:29 +00:00
marco370
a1515f40eb Restored to '6db91c5cef83702da055ec41ac10c52d783a6d44'
Replit-Restored-To: 6db91c5cef
2025-10-17 08:46:22 +00:00
marco370
fcb2020618 Saved your changes before rolling back
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 94491beb-cc63-4877-b64a-41f1e2a4c43a
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-17 08:46:16 +00:00
marco370
a900ed2755 Improve vehicle creation by handling unassigned guards correctly
Update vehicle creation API to correctly map "unassigned" guard values to null, and adjust the UI SelectItems to use "unassigned" string value.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/B8lcojv
2025-10-17 08:46:14 +00:00
marco370
6f1d83ad8f Saved your changes before starting work
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 94491beb-cc63-4877-b64a-41f1e2a4c43a
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-17 08:46:13 +00:00
marco370
6db91c5cef Update Git push configuration for exports
No changes to commit.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/B8lcojv
2025-10-17 08:38:57 +00:00
marco370
2f0831b81e Improve database backup reliability and error handling
Enhance pg_dump backup command with --clean, --if-exists, and --inserts flags for improved reliability and compatibility, and update error logging for failed backups.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/B8lcojv
2025-10-17 08:38:07 +00:00
10 changed files with 306 additions and 99 deletions

View File

@ -28,7 +28,7 @@ localPort = 42175
externalPort = 3002
[[ports]]
localPort = 42403
localPort = 43267
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, type Site } from "@shared/schema";
import { insertSiteSchema, insertServiceTypeSchema, type Site, type ServiceType } from "@shared/schema";
import { z } from "zod";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@ -35,34 +35,27 @@ 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 { Building2, Shield, Eye, MapPin, Zap, Plus, Pencil } from "lucide-react";
import * as LucideIcons from "lucide-react";
import { MapPin, Plus, Pencil } from "lucide-react";
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;
// 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;
};
type SiteForm = z.infer<typeof insertSiteSchema>;
@ -73,10 +66,16 @@ export default function Services() {
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const { data: sites = [], isLoading } = useQuery<Site[]>({
const { data: sites = [], isLoading: isLoadingSites } = 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: {
@ -183,9 +182,9 @@ export default function Services() {
};
// Calculate statistics per service type
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
const sitesForType = sites.filter(s => s.shiftType === type);
acc[type] = {
const stats = serviceTypes.reduce((acc, serviceType) => {
const sitesForType = sites.filter(s => s.shiftType === serviceType.code);
acc[serviceType.code] = {
total: sitesForType.length,
active: sitesForType.filter(s => s.isActive).length,
requiresArmed: sitesForType.filter(s => s.requiresArmed).length,
@ -214,56 +213,48 @@ export default function Services() {
) : (
<>
<div className="grid gap-6 md:grid-cols-2">
{Object.entries(serviceTypeInfo).map(([type, info]) => {
const Icon = info.icon;
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
{serviceTypes.map((serviceType) => {
const Icon = getIconComponent(serviceType.icon);
const stat = stats[serviceType.code] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
return (
<Card key={type} data-testid={`card-service-${type}`}>
<Card key={serviceType.id} data-testid={`card-service-${serviceType.code}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${info.color}`}>
<div className={`p-2 rounded-lg ${getColorClasses(serviceType.color)}`}>
<Icon className="h-6 w-6" />
</div>
<div>
<CardTitle className="text-xl">{info.label}</CardTitle>
<CardDescription className="mt-1">{info.description}</CardDescription>
<CardTitle className="text-xl">{serviceType.label}</CardTitle>
<CardDescription className="mt-1">{serviceType.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-${type}`}>
<p className="text-2xl font-semibold" data-testid={`text-total-${serviceType.code}`}>
{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-${type}`}>
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${serviceType.code}`}>
{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-${type}`}>
<p className="text-lg font-semibold" data-testid={`text-armed-${serviceType.code}`}>
{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-${type}`}>
<p className="text-lg font-semibold" data-testid={`text-driver-${serviceType.code}`}>
{stat.requiresDriver}
</p>
</div>
@ -273,15 +264,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 === type && s.location === 'roccapiemonte').length} Roccapiemonte
{sites.filter(s => s.shiftType === serviceType.code && 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 === type && s.location === 'milano').length} Milano
{sites.filter(s => s.shiftType === serviceType.code && 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 === type && s.location === 'roma').length} Roma
{sites.filter(s => s.shiftType === serviceType.code && s.location === 'roma').length} Roma
</Badge>
</div>
</div>
@ -291,10 +282,10 @@ export default function Services() {
variant="ghost"
size="sm"
className="w-full mt-4"
onClick={() => setSelectedServiceType(selectedServiceType === type ? null : type)}
data-testid={`button-view-sites-${type}`}
onClick={() => setSelectedServiceType(selectedServiceType === serviceType.code ? null : serviceType.code)}
data-testid={`button-view-sites-${serviceType.code}`}
>
{selectedServiceType === type ? "Nascondi" : "Visualizza"} Siti ({stat.total})
{selectedServiceType === serviceType.code ? "Nascondi" : "Visualizza"} Siti ({stat.total})
</Button>
)}
</CardContent>
@ -307,7 +298,7 @@ export default function Services() {
<Card>
<CardHeader>
<CardTitle>
Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label}
Siti {serviceTypes.find(st => st.code === selectedServiceType)?.label || ""}
</CardTitle>
<CardDescription>
Lista completa dei siti con questo tipo di servizio
@ -429,10 +420,9 @@ export default function Services() {
</SelectTrigger>
</FormControl>
<SelectContent>
<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>
{serviceTypes.filter(st => st.isActive).map(st => (
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
@ -616,10 +606,9 @@ export default function Services() {
</SelectTrigger>
</FormControl>
<SelectContent>
<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>
{serviceTypes.filter(st => st.isActive).map(st => (
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />

View File

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

View File

@ -135,7 +135,8 @@ else
log_info "Creo backup: $BACKUP_FILE"
# Backup con pg_dump (include schema e dati)
if pg_dump "$DATABASE_URL" > "$BACKUP_FILE" 2>/dev/null; then
# 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
# Comprimi backup
gzip "$BACKUP_FILE"
BACKUP_FILE="${BACKUP_FILE}.gz"
@ -147,7 +148,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!"
log_error "Backup database fallito! Verifica DATABASE_URL e permessi pg_dump"
exit 1
fi
fi

View File

@ -2,11 +2,21 @@
set -e
# Script di deployment automatico per VigilanzaTurni
# Uso: bash deploy/deploy.sh
# Uso:
# bash deploy/deploy.sh → deployment normale
# bash deploy/deploy.sh db → deployment + importa backup database da Git
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)"
@ -19,18 +29,54 @@ 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
# Esegui backup PostgreSQL
if command -v pg_dump &> /dev/null; then
# =================== 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
if [ -z "$PGDATABASE" ] || [ -z "$PGUSER" ]; then
echo "⚠️ Variabili DB non trovate nel .env, skip backup"
@ -46,14 +92,15 @@ if command -v pg_dump &> /dev/null; then
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
echo "🧹 Backup vecchi eliminati (retention: 30 giorni)"
fi
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
npm ci --include=dev
# Build frontend
echo "🏗️ Build frontend Vite..."

View File

@ -418,6 +418,53 @@ 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,11 +1,58 @@
import { db } from "./db";
import { users, guards, sites, vehicles, contractParameters } from "@shared/schema";
import { users, guards, sites, vehicles, contractParameters, serviceTypes } 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,6 +16,7 @@ import {
absences,
absenceAffectedShifts,
contractParameters,
serviceTypes,
type User,
type UpsertUser,
type Guard,
@ -48,6 +49,8 @@ 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";
@ -70,6 +73,13 @@ 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>;
@ -272,6 +282,35 @@ 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,6 +174,20 @@ 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", {
@ -609,6 +623,12 @@ 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,
@ -699,6 +719,9 @@ 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,7 +1,13 @@
{
"version": "1.0.8",
"lastUpdate": "2025-10-17T08:36:23.963Z",
"version": "1.0.9",
"lastUpdate": "2025-10-17T09:18:14.391Z",
"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",