Compare commits
10 Commits
4693b782cb
...
cd986635e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd986635e4 | ||
|
|
a5355ed881 | ||
|
|
204717cd9d | ||
|
|
ab7b3c7f90 | ||
|
|
a1515f40eb | ||
|
|
fcb2020618 | ||
|
|
a900ed2755 | ||
|
|
6f1d83ad8f | ||
|
|
6db91c5cef | ||
|
|
2f0831b81e |
2
.replit
2
.replit
@ -28,7 +28,7 @@ localPort = 42175
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 42403
|
||||
localPort = 43267
|
||||
externalPort = 3003
|
||||
|
||||
[env]
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,16 +29,52 @@ 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
|
||||
@ -49,11 +95,12 @@ if command -v pg_dump &> /dev/null; then
|
||||
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..."
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
10
version.json
10
version.json
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user