diff --git a/.replit b/.replit index a2fd13c..048618b 100644 --- a/.replit +++ b/.replit @@ -23,10 +23,6 @@ externalPort = 3001 localPort = 41343 externalPort = 3000 -[[ports]] -localPort = 41607 -externalPort = 3003 - [[ports]] localPort = 42175 externalPort = 3002 diff --git a/client/src/pages/dashboard.tsx b/client/src/pages/dashboard.tsx index b73ba9c..def4249 100644 --- a/client/src/pages/dashboard.tsx +++ b/client/src/pages/dashboard.tsx @@ -1,9 +1,11 @@ +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/hooks/useAuth"; import { KPICard } from "@/components/kpi-card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { StatusBadge } from "@/components/status-badge"; -import { Users, Calendar, MapPin, AlertTriangle, Clock, CheckCircle } from "lucide-react"; +import { Users, Calendar, MapPin, AlertTriangle, Clock, CheckCircle, Building2 } from "lucide-react"; import { ShiftWithDetails, GuardWithCertifications, Site } from "@shared/schema"; import { formatDistanceToNow, format } from "date-fns"; import { it } from "date-fns/locale"; @@ -11,6 +13,7 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Dashboard() { const { user } = useAuth(); + const [selectedLocation, setSelectedLocation] = useState("all"); const { data: shifts, isLoading: shiftsLoading } = useQuery({ queryKey: ["/api/shifts/active"], @@ -24,25 +27,64 @@ export default function Dashboard() { queryKey: ["/api/sites"], }); + // Filter data by location + const filteredGuards = selectedLocation === "all" + ? guards + : guards?.filter(g => g.location === selectedLocation); + + const filteredSites = selectedLocation === "all" + ? sites + : sites?.filter(s => s.location === selectedLocation); + + const filteredShifts = selectedLocation === "all" + ? shifts + : shifts?.filter(s => { + const site = sites?.find(site => site.id === s.siteId); + return site?.location === selectedLocation; + }); + // Calculate KPIs - const activeShifts = shifts?.filter(s => s.status === "active").length || 0; - const totalGuards = guards?.length || 0; - const activeSites = sites?.filter(s => s.isActive).length || 0; + const activeShifts = filteredShifts?.filter(s => s.status === "active").length || 0; + const totalGuards = filteredGuards?.length || 0; + const activeSites = filteredSites?.filter(s => s.isActive).length || 0; // Expiring certifications (next 30 days) - const expiringCerts = guards?.flatMap(g => + const expiringCerts = filteredGuards?.flatMap(g => g.certifications.filter(c => c.status === "expiring_soon") ).length || 0; const isLoading = shiftsLoading || guardsLoading || sitesLoading; + const locationLabels: Record = { + all: "Tutte le Sedi", + roccapiemonte: "Roccapiemonte", + milano: "Milano", + roma: "Roma" + }; + return (
-
-

Dashboard Operativa

-

- Benvenuto, {user?.firstName} {user?.lastName} -

+
+
+

Dashboard Operativa

+

+ Benvenuto, {user?.firstName} {user?.lastName} +

+
+
+ + +
{/* KPI Cards */} @@ -103,9 +145,9 @@ export default function Dashboard() {
- ) : shifts && shifts.length > 0 ? ( + ) : filteredShifts && filteredShifts.length > 0 ? (
- {shifts.slice(0, 5).map((shift) => ( + {filteredShifts.slice(0, 5).map((shift) => (
- ) : guards ? ( + ) : filteredGuards ? (
- {guards + {filteredGuards .flatMap(guard => guard.certifications .filter(c => c.status === "expiring_soon" || c.status === "expired") @@ -176,7 +218,7 @@ export default function Dashboard() {
))} - {guards.flatMap(g => g.certifications.filter(c => c.status !== "valid")).length === 0 && ( + {filteredGuards.flatMap(g => g.certifications.filter(c => c.status !== "valid")).length === 0 && (
Tutte le certificazioni sono valide diff --git a/client/src/pages/parameters.tsx b/client/src/pages/parameters.tsx index dc0ab3f..df00a39 100644 --- a/client/src/pages/parameters.tsx +++ b/client/src/pages/parameters.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; import { useToast } from "@/hooks/use-toast"; import { Loader2, Save, Settings } from "lucide-react"; import type { ContractParameters } from "@shared/schema"; @@ -345,6 +346,61 @@ export default function Parameters() { + {/* Buoni Pasto */} + + + Buoni Pasto + Configurazione ticket restaurant per turni superiori a soglia ore + + +
+
+ + setFormData({ ...formData, mealVoucherEnabled: checked })} + disabled={!isEditing} + data-testid="switch-meal-voucher-enabled" + /> +
+

+ Abilita emissione buoni pasto automatici +

+
+ +
+ + setFormData({ ...formData, mealVoucherAfterHours: parseInt(e.target.value) })} + disabled={!isEditing || !formData.mealVoucherEnabled} + data-testid="input-meal-voucher-after-hours" + /> +

+ Ore di turno necessarie per diritto al buono +

+
+ +
+ + setFormData({ ...formData, mealVoucherAmount: parseInt(e.target.value) })} + disabled={!isEditing || !formData.mealVoucherEnabled} + data-testid="input-meal-voucher-amount" + /> +

+ Valore nominale ticket (facoltativo) +

+
+
+
+ {/* Tipo Contratto */} diff --git a/client/src/pages/shifts.tsx b/client/src/pages/shifts.tsx index c862c44..248250d 100644 --- a/client/src/pages/shifts.tsx +++ b/client/src/pages/shifts.tsx @@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { insertShiftFormSchema } from "@shared/schema"; -import { Plus, Calendar, MapPin, Users, Clock, UserPlus, X, Shield, Car, Heart, Flame, Pencil } from "lucide-react"; +import { Plus, Calendar, MapPin, Users, Clock, UserPlus, X, Shield, Car, Heart, Flame, Pencil, Building2 } from "lucide-react"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { StatusBadge } from "@/components/status-badge"; @@ -24,6 +24,7 @@ export default function Shifts() { const [selectedShift, setSelectedShift] = useState(null); const [isAssignDialogOpen, setIsAssignDialogOpen] = useState(false); const [editingShift, setEditingShift] = useState(null); + const [selectedLocation, setSelectedLocation] = useState("all"); const { data: shifts, isLoading: shiftsLoading } = useQuery({ queryKey: ["/api/shifts"], @@ -37,6 +38,22 @@ export default function Shifts() { queryKey: ["/api/guards"], }); + // Filter data by location + const filteredShifts = selectedLocation === "all" + ? shifts + : shifts?.filter(s => { + const site = sites?.find(site => site.id === s.siteId); + return site?.location === selectedLocation; + }); + + const filteredSites = selectedLocation === "all" + ? sites + : sites?.filter(s => s.location === selectedLocation); + + const filteredGuards = selectedLocation === "all" + ? guards + : guards?.filter(g => g.location === selectedLocation); + const form = useForm({ resolver: zodResolver(insertShiftFormSchema), defaultValues: { @@ -238,13 +255,28 @@ export default function Shifts() { Calendario 24/7 con assegnazione guardie

- - - - +
+
+ + +
+ + + + Nuovo Turno @@ -267,7 +299,7 @@ export default function Shifts() { - {sites?.map((site) => ( + {filteredSites?.map((site) => ( {site.name} @@ -344,6 +376,7 @@ export default function Shifts() { +
{shiftsLoading ? ( @@ -352,9 +385,9 @@ export default function Shifts() {
- ) : shifts && shifts.length > 0 ? ( + ) : filteredShifts && filteredShifts.length > 0 ? (
- {shifts.map((shift) => ( + {filteredShifts.map((shift) => (
@@ -493,7 +526,7 @@ export default function Shifts() {

Guardie Disponibili

{guards && guards.length > 0 ? (
- {guards.map((guard) => { + {filteredGuards?.map((guard) => { const assigned = isGuardAssigned(guard.id); const canAssign = canGuardBeAssigned(guard); @@ -585,7 +618,7 @@ export default function Shifts() { - {sites?.map((site) => ( + {filteredSites?.map((site) => ( {site.name} diff --git a/server/seed.ts b/server/seed.ts new file mode 100644 index 0000000..31a86cb --- /dev/null +++ b/server/seed.ts @@ -0,0 +1,225 @@ +import { db } from "./db"; +import { users, guards, sites, vehicles } from "@shared/schema"; +import { eq } from "drizzle-orm"; +import bcrypt from "bcrypt"; + +async function seed() { + console.log("🌱 Avvio seed database multi-sede..."); + + // Locations + const locations = ["roccapiemonte", "milano", "roma"] as const; + const locationNames = { + roccapiemonte: "Roccapiemonte", + milano: "Milano", + roma: "Roma" + }; + + // Cleanup existing data (optional - comment out to preserve existing data) + // await db.delete(guards); + // await db.delete(sites); + // await db.delete(vehicles); + + console.log("πŸ‘₯ Creazione guardie per ogni sede..."); + + // Create 10 guards per location + const guardNames = [ + "Marco Rossi", "Luca Bianchi", "Giuseppe Verdi", "Francesco Romano", + "Alessandro Russo", "Andrea Marino", "Matteo Ferrari", "Lorenzo Conti", + "Davide Ricci", "Simone Moretti" + ]; + + for (const location of locations) { + for (let i = 0; i < 10; i++) { + const fullName = guardNames[i]; + const [firstName, ...lastNameParts] = fullName.split(" "); + const lastName = lastNameParts.join(" "); + const email = `${fullName.toLowerCase().replace(" ", ".")}@${location}.vt.alfacom.it`; + const badgeNumber = `${location.substring(0, 3).toUpperCase()}${String(i + 1).padStart(3, "0")}`; + + // Check if user exists + const existingUser = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + let userId: string; + + if (existingUser.length > 0) { + userId = existingUser[0].id; + console.log(` βœ“ Utente esistente: ${email}`); + } else { + // Create user + const hashedPassword = await bcrypt.hash("guard123", 10); + const [newUser] = await db + .insert(users) + .values({ + email, + firstName, + lastName, + passwordHash: hashedPassword, + role: "guard" + }) + .returning(); + userId = newUser.id; + console.log(` + Creato utente: ${email}`); + } + + // Check if guard exists + const existingGuard = await db + .select() + .from(guards) + .where(eq(guards.badgeNumber, badgeNumber)) + .limit(1); + + if (existingGuard.length === 0) { + await db.insert(guards).values({ + userId, + badgeNumber, + phoneNumber: `+39 ${330 + i} ${Math.floor(Math.random() * 1000000)}`, + location, + isArmed: i % 3 === 0, // 1 su 3 Γ¨ armato + hasFireSafety: i % 2 === 0, // 1 su 2 ha antincendio + hasFirstAid: i % 4 === 0, // 1 su 4 ha primo soccorso + hasDriverLicense: i % 2 === 1, // 1 su 2 ha patente + languages: i === 0 ? ["italiano", "inglese"] : ["italiano"] + }); + console.log(` + Creata guardia: ${badgeNumber} - ${name} (${locationNames[location]})`); + } else { + console.log(` βœ“ Guardia esistente: ${badgeNumber}`); + } + } + } + + console.log("\n🏒 Creazione clienti per ogni sede..."); + + // Create 10 clients per location + const companyNames = [ + "Banca Centrale", "Ospedale San Marco", "Centro Commerciale Europa", + "Uffici Postali", "Museo Arte Moderna", "Palazzo Comunale", + "Stazione Ferroviaria", "Aeroporto Internazionale", "UniversitΓ  Statale", + "Tribunale Civile" + ]; + + for (const location of locations) { + for (let i = 0; i < 10; i++) { + const companyName = companyNames[i]; + const email = `${companyName.toLowerCase().replace(/ /g, ".")}@${location}.clienti.vt.it`; + + // Check if client user exists + const existingClient = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + let clientId: string; + + if (existingClient.length > 0) { + clientId = existingClient[0].id; + console.log(` βœ“ Cliente esistente: ${email}`); + } else { + const hashedPassword = await bcrypt.hash("client123", 10); + const [newClient] = await db + .insert(users) + .values({ + email, + firstName: companyName, + lastName: locationNames[location], + passwordHash: hashedPassword, + role: "client" + }) + .returning(); + clientId = newClient.id; + console.log(` + Creato cliente: ${email}`); + } + + // Check if site exists + const siteName = `${companyName} - ${locationNames[location]}`; + const existingSite = await db + .select() + .from(sites) + .where(eq(sites.name, siteName)) + .limit(1); + + if (existingSite.length === 0) { + const shiftTypes = ["fixed_post", "patrol", "night_inspection", "quick_response"] as const; + await db.insert(sites).values({ + name: siteName, + address: `Via ${companyName} ${i + 1}, ${locationNames[location]}`, + clientId, + location, + shiftType: shiftTypes[i % 4], + minGuards: Math.floor(Math.random() * 3) + 1, + requiresArmed: i % 3 === 0, + requiresDriverLicense: i % 4 === 0, + isActive: true + }); + console.log(` + Creato sito: ${siteName}`); + } else { + console.log(` βœ“ Sito esistente: ${siteName}`); + } + } + } + + console.log("\nπŸš— Creazione automezzi per ogni sede..."); + + // Create vehicles per location + const vehicleBrands = [ + { brand: "Fiat", model: "Punto", type: "car" }, + { brand: "Volkswagen", model: "Polo", type: "car" }, + { brand: "Ford", model: "Transit", type: "van" }, + { brand: "Mercedes", model: "Sprinter", type: "van" }, + { brand: "BMW", model: "GS 750", type: "motorcycle" }, + ] as const; + + for (const location of locations) { + for (let i = 0; i < 5; i++) { + const vehicle = vehicleBrands[i]; + const licensePlate = `${location.substring(0, 2).toUpperCase()}${String(Math.floor(Math.random() * 1000)).padStart(3, "0")}${String.fromCharCode(65 + Math.floor(Math.random() * 26))}${String.fromCharCode(65 + Math.floor(Math.random() * 26))}`; + + // Check if vehicle exists + const existingVehicle = await db + .select() + .from(vehicles) + .where(eq(vehicles.licensePlate, licensePlate)) + .limit(1); + + if (existingVehicle.length === 0) { + await db.insert(vehicles).values({ + licensePlate, + brand: vehicle.brand, + model: vehicle.model, + vehicleType: vehicle.type, + year: 2018 + Math.floor(Math.random() * 6), + location, + status: i === 0 ? "in_use" : "available", + mileage: Math.floor(Math.random() * 100000) + 10000 + }); + console.log(` + Creato automezzo: ${licensePlate} - ${vehicle.brand} ${vehicle.model} (${locationNames[location]})`); + } else { + console.log(` βœ“ Automezzo esistente: ${licensePlate}`); + } + } + } + + console.log("\nβœ… Seed completato!"); + console.log(` +πŸ“Š Riepilogo: +- 30 guardie totali (10 per sede) +- 30 siti/clienti totali (10 per sede) +- 15 automezzi totali (5 per sede) + +πŸ” Credenziali: +- Guardie: *.guardia@[sede].vt.alfacom.it / guard123 +- Clienti: *@[sede].clienti.vt.it / client123 +- Admin: admin@vt.alfacom.it / admin123 + `); + + process.exit(0); +} + +seed().catch((error) => { + console.error("❌ Errore seed:", error); + process.exit(1); +});