From a5355ed881898e85618fbb0140413a0ba5c45ba5 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Fri, 17 Oct 2025 09:12:29 +0000 Subject: [PATCH] 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 --- .replit | 4 ++ client/src/pages/services.tsx | 121 ++++++++++++++++------------------ client/src/pages/vehicles.tsx | 16 +++-- server/routes.ts | 47 +++++++++++++ server/seed.ts | 49 +++++++++++++- server/storage.ts | 39 +++++++++++ shared/schema.ts | 23 +++++++ 7 files changed, 228 insertions(+), 71 deletions(-) diff --git a/.replit b/.replit index 048618b..c50bc15 100644 --- a/.replit +++ b/.replit @@ -27,6 +27,10 @@ externalPort = 3000 localPort = 42175 externalPort = 3002 +[[ports]] +localPort = 43267 +externalPort = 3003 + [env] PORT = "5000" diff --git a/client/src/pages/services.tsx b/client/src/pages/services.tsx index 5fd4856..57a3ecd 100644 --- a/client/src/pages/services.tsx +++ b/client/src/pages/services.tsx @@ -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 = { + 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; @@ -73,10 +66,16 @@ export default function Services() { const [editDialogOpen, setEditDialogOpen] = useState(false); const [selectedSite, setSelectedSite] = useState(null); - const { data: sites = [], isLoading } = useQuery({ + const { data: sites = [], isLoading: isLoadingSites } = useQuery({ queryKey: ["/api/sites"], }); + const { data: serviceTypes = [], isLoading: isLoadingServiceTypes } = useQuery({ + queryKey: ["/api/service-types"], + }); + + const isLoading = isLoadingSites || isLoadingServiceTypes; + const createForm = useForm({ 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() { ) : ( <>
- {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 ( - +
-
+
- {info.label} - {info.description} + {serviceType.label} + {serviceType.description}
-

Siti Totali

-

+

{stat.total}

Attivi

-

+

{stat.active}

Richiedono Armati

-

+

{stat.requiresArmed}

Richiedono Patente

-

+

{stat.requiresDriver}

@@ -273,15 +264,15 @@ export default function Services() {
- {sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte + {sites.filter(s => s.shiftType === serviceType.code && s.location === 'roccapiemonte').length} Roccapiemonte - {sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano + {sites.filter(s => s.shiftType === serviceType.code && s.location === 'milano').length} Milano - {sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma + {sites.filter(s => s.shiftType === serviceType.code && s.location === 'roma').length} Roma
@@ -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}) )}
@@ -307,7 +298,7 @@ export default function Services() { - Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label} + Siti {serviceTypes.find(st => st.code === selectedServiceType)?.label || ""} Lista completa dei siti con questo tipo di servizio @@ -429,10 +420,9 @@ export default function Services() { - Presidio Fisso - Pattugliamento - Ispettorato Notturno - Pronto Intervento + {serviceTypes.filter(st => st.isActive).map(st => ( + {st.label} + ))} @@ -616,10 +606,9 @@ export default function Services() { - Presidio Fisso - Pattugliamento - Ispettorato Notturno - Pronto Intervento + {serviceTypes.filter(st => st.isActive).map(st => ( + {st.label} + ))} diff --git a/client/src/pages/vehicles.tsx b/client/src/pages/vehicles.tsx index 82f35f0..4c092f5 100644 --- a/client/src/pages/vehicles.tsx +++ b/client/src/pages/vehicles.tsx @@ -511,14 +511,18 @@ export default function Vehicles() { render={({ field }) => ( Assegnato a - field.onChange(value === "none" ? null : value)} + value={field.value || "none"} + data-testid="select-create-guard" + > - Nessuno + Nessuno {guards?.map(guard => ( {guard.badgeNumber} @@ -738,14 +742,18 @@ export default function Vehicles() { render={({ field }) => ( Assegnato a - field.onChange(value === "none" ? null : value)} + value={field.value || "none"} + data-testid="select-edit-guard" + > - Nessuno + Nessuno {guards?.map(guard => ( {guard.badgeNumber} diff --git a/server/routes.ts b/server/routes.ts index 4164d5a..49ba748 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -418,6 +418,53 @@ export async function registerRoutes(app: Express): Promise { } }); + // ============= 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 { diff --git a/server/seed.ts b/server/seed.ts index bf1a153..a5c5e7d 100644 --- a/server/seed.ts +++ b/server/seed.ts @@ -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); diff --git a/server/storage.ts b/server/storage.ts index 9a2d919..2ba1d25 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -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; updateCertificationStatus(id: string, status: "valid" | "expiring_soon" | "expired"): Promise; + // Service Type operations + getAllServiceTypes(): Promise; + getServiceType(id: string): Promise; + createServiceType(serviceType: InsertServiceType): Promise; + updateServiceType(id: string, serviceType: Partial): Promise; + deleteServiceType(id: string): Promise; + // Site operations getAllSites(): Promise; getSite(id: string): Promise; @@ -272,6 +282,35 @@ export class DatabaseStorage implements IStorage { .where(eq(certifications.id, id)); } + // Service Type operations + async getAllServiceTypes(): Promise { + return await db.select().from(serviceTypes).orderBy(desc(serviceTypes.createdAt)); + } + + async getServiceType(id: string): Promise { + const [serviceType] = await db.select().from(serviceTypes).where(eq(serviceTypes.id, id)); + return serviceType; + } + + async createServiceType(serviceType: InsertServiceType): Promise { + const [newServiceType] = await db.insert(serviceTypes).values(serviceType).returning(); + return newServiceType; + } + + async updateServiceType(id: string, serviceTypeData: Partial): Promise { + const [updated] = await db + .update(serviceTypes) + .set({ ...serviceTypeData, updatedAt: new Date() }) + .where(eq(serviceTypes.id, id)) + .returning(); + return updated; + } + + async deleteServiceType(id: string): Promise { + const [deleted] = await db.delete(serviceTypes).where(eq(serviceTypes.id, id)).returning(); + return deleted; + } + // Site operations async getAllSites(): Promise { return await db.select().from(sites); diff --git a/shared/schema.ts b/shared/schema.ts index 485bd5e..15e3a47 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -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; export type Vehicle = typeof vehicles.$inferSelect; +export type InsertServiceType = z.infer; +export type ServiceType = typeof serviceTypes.$inferSelect; + export type InsertSite = z.infer; export type Site = typeof sites.$inferSelect;