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 externalPort = 3002
[[ports]] [[ports]]
localPort = 42403 localPort = 43267
externalPort = 3003 externalPort = 3003
[env] [env]

View File

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

View File

@ -511,14 +511,18 @@ export default function Vehicles() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Assegnato a</FormLabel> <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> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleziona guardia" /> <SelectValue placeholder="Seleziona guardia" />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="">Nessuno</SelectItem> <SelectItem value="none">Nessuno</SelectItem>
{guards?.map(guard => ( {guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}> <SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber} {guard.badgeNumber}
@ -738,14 +742,18 @@ export default function Vehicles() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Assegnato a</FormLabel> <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> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleziona guardia" /> <SelectValue placeholder="Seleziona guardia" />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="">Nessuno</SelectItem> <SelectItem value="none">Nessuno</SelectItem>
{guards?.map(guard => ( {guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}> <SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber} {guard.badgeNumber}

View File

@ -135,7 +135,8 @@ else
log_info "Creo backup: $BACKUP_FILE" log_info "Creo backup: $BACKUP_FILE"
# Backup con pg_dump (include schema e dati) # 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 # Comprimi backup
gzip "$BACKUP_FILE" gzip "$BACKUP_FILE"
BACKUP_FILE="${BACKUP_FILE}.gz" BACKUP_FILE="${BACKUP_FILE}.gz"
@ -147,7 +148,7 @@ else
log_info "Pulizia backup vecchi (mantengo ultimi 10)..." 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 ls -t $BACKUP_DIR/*.sql.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true
else else
log_error "Backup database fallito!" log_error "Backup database fallito! Verifica DATABASE_URL e permessi pg_dump"
exit 1 exit 1
fi fi
fi fi

View File

@ -2,11 +2,21 @@
set -e set -e
# Script di deployment automatico per VigilanzaTurni # 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_DIR="/var/www/vigilanza-turni"
APP_NAME="vigilanza-turni" APP_NAME="vigilanza-turni"
BACKUP_DIR="/var/backups/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)" echo "🚀 Deployment VigilanzaTurni - $(date)"
@ -19,16 +29,52 @@ if [ -d .git ]; then
git pull origin main || true git pull origin main || true
fi 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 # Load env vars
if [ -f .env ]; then if [ -f .env ]; then
source .env source .env
fi 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 # Esegui backup PostgreSQL
if command -v pg_dump &> /dev/null; then if command -v pg_dump &> /dev/null; then
# Controlla che le variabili siano definite # Controlla che le variabili siano definite
@ -49,11 +95,12 @@ if command -v pg_dump &> /dev/null; then
else else
echo "⚠️ pg_dump non trovato, skip backup" echo "⚠️ pg_dump non trovato, skip backup"
fi fi
fi
# =================== BUILD & DEPLOY =================== # =================== BUILD & DEPLOY ===================
# Installa TUTTE le dipendenze (serve per build e migrations) # Installa TUTTE le dipendenze (serve per build e migrations)
echo "📥 Installazione dipendenze (include devDependencies)..." echo "📥 Installazione dipendenze (include devDependencies)..."
npm ci npm ci --include=dev
# Build frontend # Build frontend
echo "🏗️ Build frontend Vite..." 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 ============= // ============= SITE ROUTES =============
app.get("/api/sites", isAuthenticated, async (req, res) => { app.get("/api/sites", isAuthenticated, async (req, res) => {
try { try {

View File

@ -1,11 +1,58 @@
import { db } from "./db"; 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 { eq } from "drizzle-orm";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
async function seed() { async function seed() {
console.log("🌱 Avvio seed database multi-sede..."); 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 // Create CCNL contract parameters
console.log("📋 Creazione parametri contrattuali CCNL..."); console.log("📋 Creazione parametri contrattuali CCNL...");
const existingParams = await db.select().from(contractParameters).limit(1); const existingParams = await db.select().from(contractParameters).limit(1);

View File

@ -16,6 +16,7 @@ import {
absences, absences,
absenceAffectedShifts, absenceAffectedShifts,
contractParameters, contractParameters,
serviceTypes,
type User, type User,
type UpsertUser, type UpsertUser,
type Guard, type Guard,
@ -48,6 +49,8 @@ import {
type InsertAbsenceAffectedShift, type InsertAbsenceAffectedShift,
type ContractParameters, type ContractParameters,
type InsertContractParameters, type InsertContractParameters,
type ServiceType,
type InsertServiceType,
} from "@shared/schema"; } from "@shared/schema";
import { db } from "./db"; import { db } from "./db";
import { eq, and, gte, lte, desc } from "drizzle-orm"; import { eq, and, gte, lte, desc } from "drizzle-orm";
@ -70,6 +73,13 @@ export interface IStorage {
createCertification(cert: InsertCertification): Promise<Certification>; createCertification(cert: InsertCertification): Promise<Certification>;
updateCertificationStatus(id: string, status: "valid" | "expiring_soon" | "expired"): Promise<void>; 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 // Site operations
getAllSites(): Promise<Site[]>; getAllSites(): Promise<Site[]>;
getSite(id: string): Promise<Site | undefined>; getSite(id: string): Promise<Site | undefined>;
@ -272,6 +282,35 @@ export class DatabaseStorage implements IStorage {
.where(eq(certifications.id, id)); .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 // Site operations
async getAllSites(): Promise<Site[]> { async getAllSites(): Promise<Site[]> {
return await db.select().from(sites); return await db.select().from(sites);

View File

@ -174,6 +174,20 @@ export const vehicles = pgTable("vehicles", {
updatedAt: timestamp("updated_at").defaultNow(), 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 ============= // ============= SITES & CONTRACTS =============
export const sites = pgTable("sites", { export const sites = pgTable("sites", {
@ -609,6 +623,12 @@ export const insertVehicleSchema = createInsertSchema(vehicles).omit({
updatedAt: true, updatedAt: true,
}); });
export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertSiteSchema = createInsertSchema(sites).omit({ export const insertSiteSchema = createInsertSchema(sites).omit({
id: true, id: true,
createdAt: true, createdAt: true,
@ -699,6 +719,9 @@ export type Certification = typeof certifications.$inferSelect;
export type InsertVehicle = z.infer<typeof insertVehicleSchema>; export type InsertVehicle = z.infer<typeof insertVehicleSchema>;
export type Vehicle = typeof vehicles.$inferSelect; 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 InsertSite = z.infer<typeof insertSiteSchema>;
export type Site = typeof sites.$inferSelect; export type Site = typeof sites.$inferSelect;

View File

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