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;