Compare commits
No commits in common. "cd986635e4a67a3ff5e36f9a41e8d928feb216f8" and "4693b782cb64b9e5963992771f283cb2e8f5d320" have entirely different histories.
cd986635e4
...
4693b782cb
2
.replit
2
.replit
@ -28,7 +28,7 @@ localPort = 42175
|
|||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43267
|
localPort = 42403
|
||||||
externalPort = 3003
|
externalPort = 3003
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
|
|||||||
@ -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, insertServiceTypeSchema, type Site, type ServiceType } from "@shared/schema";
|
import { insertSiteSchema, type Site } 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,27 +35,34 @@ 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 * as LucideIcons from "lucide-react";
|
import { Building2, Shield, Eye, MapPin, Zap, Plus, Pencil } from "lucide-react";
|
||||||
import { MapPin, Plus, Pencil } from "lucide-react";
|
|
||||||
|
|
||||||
// Helper to get icon component from name
|
const serviceTypeInfo = {
|
||||||
const getIconComponent = (iconName: string) => {
|
fixed_post: {
|
||||||
const Icon = (LucideIcons as any)[iconName];
|
label: "Presidio Fisso",
|
||||||
return Icon || LucideIcons.Building2;
|
description: "Guardia fissa presso una struttura",
|
||||||
};
|
icon: Building2,
|
||||||
|
color: "bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||||
// Helper to get color classes from color name
|
},
|
||||||
const getColorClasses = (color: string) => {
|
patrol: {
|
||||||
const colorMap: Record<string, string> = {
|
label: "Pattugliamento",
|
||||||
blue: "bg-blue-500/10 text-blue-500 border-blue-500/20",
|
description: "Ronde e controlli su area",
|
||||||
green: "bg-green-500/10 text-green-500 border-green-500/20",
|
icon: Eye,
|
||||||
purple: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
color: "bg-green-500/10 text-green-500 border-green-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",
|
night_inspection: {
|
||||||
yellow: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
label: "Ispettorato Notturno",
|
||||||
};
|
description: "Controlli notturni programmati",
|
||||||
return colorMap[color] || colorMap.blue;
|
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>;
|
type SiteForm = z.infer<typeof insertSiteSchema>;
|
||||||
|
|
||||||
@ -66,16 +73,10 @@ 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: isLoadingSites } = useQuery<Site[]>({
|
const { data: sites = [], isLoading } = 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: {
|
||||||
@ -182,9 +183,9 @@ export default function Services() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Calculate statistics per service type
|
// Calculate statistics per service type
|
||||||
const stats = serviceTypes.reduce((acc, serviceType) => {
|
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
|
||||||
const sitesForType = sites.filter(s => s.shiftType === serviceType.code);
|
const sitesForType = sites.filter(s => s.shiftType === type);
|
||||||
acc[serviceType.code] = {
|
acc[type] = {
|
||||||
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,
|
||||||
@ -213,48 +214,56 @@ export default function Services() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{serviceTypes.map((serviceType) => {
|
{Object.entries(serviceTypeInfo).map(([type, info]) => {
|
||||||
const Icon = getIconComponent(serviceType.icon);
|
const Icon = info.icon;
|
||||||
const stat = stats[serviceType.code] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
|
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={serviceType.id} data-testid={`card-service-${serviceType.code}`}>
|
<Card key={type} data-testid={`card-service-${type}`}>
|
||||||
<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 ${getColorClasses(serviceType.color)}`}>
|
<div className={`p-2 rounded-lg ${info.color}`}>
|
||||||
<Icon className="h-6 w-6" />
|
<Icon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">{serviceType.label}</CardTitle>
|
<CardTitle className="text-xl">{info.label}</CardTitle>
|
||||||
<CardDescription className="mt-1">{serviceType.description}</CardDescription>
|
<CardDescription className="mt-1">{info.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-${serviceType.code}`}>
|
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}>
|
||||||
{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-${serviceType.code}`}>
|
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
|
||||||
{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-${serviceType.code}`}>
|
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}>
|
||||||
{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-${serviceType.code}`}>
|
<p className="text-lg font-semibold" data-testid={`text-driver-${type}`}>
|
||||||
{stat.requiresDriver}
|
{stat.requiresDriver}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -264,15 +273,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 === serviceType.code && s.location === 'roccapiemonte').length} Roccapiemonte
|
{sites.filter(s => s.shiftType === type && 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 === serviceType.code && s.location === 'milano').length} Milano
|
{sites.filter(s => s.shiftType === type && 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 === serviceType.code && s.location === 'roma').length} Roma
|
{sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -282,10 +291,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 === serviceType.code ? null : serviceType.code)}
|
onClick={() => setSelectedServiceType(selectedServiceType === type ? null : type)}
|
||||||
data-testid={`button-view-sites-${serviceType.code}`}
|
data-testid={`button-view-sites-${type}`}
|
||||||
>
|
>
|
||||||
{selectedServiceType === serviceType.code ? "Nascondi" : "Visualizza"} Siti ({stat.total})
|
{selectedServiceType === type ? "Nascondi" : "Visualizza"} Siti ({stat.total})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -298,7 +307,7 @@ export default function Services() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
Siti {serviceTypes.find(st => st.code === selectedServiceType)?.label || ""}
|
Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Lista completa dei siti con questo tipo di servizio
|
Lista completa dei siti con questo tipo di servizio
|
||||||
@ -420,9 +429,10 @@ export default function Services() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{serviceTypes.filter(st => st.isActive).map(st => (
|
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
|
||||||
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
|
<SelectItem value="patrol">Pattugliamento</SelectItem>
|
||||||
))}
|
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
|
||||||
|
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -606,9 +616,10 @@ export default function Services() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{serviceTypes.filter(st => st.isActive).map(st => (
|
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
|
||||||
<SelectItem key={st.id} value={st.code}>{st.label}</SelectItem>
|
<SelectItem value="patrol">Pattugliamento</SelectItem>
|
||||||
))}
|
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
|
||||||
|
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -511,18 +511,14 @@ export default function Vehicles() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Assegnato a</FormLabel>
|
<FormLabel>Assegnato a</FormLabel>
|
||||||
<Select
|
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-create-guard">
|
||||||
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="none">Nessuno</SelectItem>
|
<SelectItem value="">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}
|
||||||
@ -742,18 +738,14 @@ export default function Vehicles() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Assegnato a</FormLabel>
|
<FormLabel>Assegnato a</FormLabel>
|
||||||
<Select
|
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-edit-guard">
|
||||||
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="none">Nessuno</SelectItem>
|
<SelectItem value="">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}
|
||||||
|
|||||||
@ -135,8 +135,7 @@ 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)
|
||||||
# Opzioni: --clean (DROP before CREATE), --if-exists (no error if not exists), --inserts (INSERT statements per compatibilità)
|
if pg_dump "$DATABASE_URL" > "$BACKUP_FILE" 2>/dev/null; then
|
||||||
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"
|
||||||
@ -148,7 +147,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! Verifica DATABASE_URL e permessi pg_dump"
|
log_error "Backup database fallito!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -2,21 +2,11 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Script di deployment automatico per VigilanzaTurni
|
# Script di deployment automatico per VigilanzaTurni
|
||||||
# Uso:
|
# Uso: bash deploy/deploy.sh
|
||||||
# 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)"
|
||||||
|
|
||||||
@ -29,54 +19,18 @@ 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) ===================
|
# Esegui backup PostgreSQL
|
||||||
if [ "$IMPORT_DB" = true ]; then
|
if command -v pg_dump &> /dev/null; 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
|
# Controlla che le variabili siano definite
|
||||||
if [ -z "$PGDATABASE" ] || [ -z "$PGUSER" ]; then
|
if [ -z "$PGDATABASE" ] || [ -z "$PGUSER" ]; then
|
||||||
echo "⚠️ Variabili DB non trovate nel .env, skip backup"
|
echo "⚠️ Variabili DB non trovate nel .env, skip backup"
|
||||||
@ -92,15 +46,14 @@ else
|
|||||||
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
|
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
|
||||||
echo "🧹 Backup vecchi eliminati (retention: 30 giorni)"
|
echo "🧹 Backup vecchi eliminati (retention: 30 giorni)"
|
||||||
fi
|
fi
|
||||||
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 --include=dev
|
npm ci
|
||||||
|
|
||||||
# Build frontend
|
# Build frontend
|
||||||
echo "🏗️ Build frontend Vite..."
|
echo "🏗️ Build frontend Vite..."
|
||||||
|
|||||||
@ -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 =============
|
// ============= SITE ROUTES =============
|
||||||
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,58 +1,11 @@
|
|||||||
import { db } from "./db";
|
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 { 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);
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
absences,
|
absences,
|
||||||
absenceAffectedShifts,
|
absenceAffectedShifts,
|
||||||
contractParameters,
|
contractParameters,
|
||||||
serviceTypes,
|
|
||||||
type User,
|
type User,
|
||||||
type UpsertUser,
|
type UpsertUser,
|
||||||
type Guard,
|
type Guard,
|
||||||
@ -49,8 +48,6 @@ 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";
|
||||||
@ -73,13 +70,6 @@ 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>;
|
||||||
@ -282,35 +272,6 @@ 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);
|
||||||
|
|||||||
@ -174,20 +174,6 @@ 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", {
|
||||||
@ -623,12 +609,6 @@ 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,
|
||||||
@ -719,9 +699,6 @@ 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;
|
||||||
|
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.9",
|
"version": "1.0.8",
|
||||||
"lastUpdate": "2025-10-17T09:18:14.391Z",
|
"lastUpdate": "2025-10-17T08:36:23.963Z",
|
||||||
"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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user