import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { format, startOfWeek, addWeeks } from "date-fns"; import { it } from "date-fns/locale"; import { useLocation } from "wouter"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import type { GuardAvailability } from "@shared/schema"; interface GuardWithHours { guardId: string; guardName: string; badgeNumber: string; hours: number; } interface Vehicle { vehicleId: string; licensePlate: string; brand: string; model: string; } interface SiteData { siteId: string; siteName: string; serviceType: string; minGuards: number; guards: GuardWithHours[]; vehicles: Vehicle[]; totalShiftHours: number; guardsAssigned: number; missingGuards: number; shiftsCount: number; } interface DayData { date: string; dayOfWeek: string; sites: SiteData[]; } interface GeneralPlanningResponse { weekStart: string; weekEnd: string; location: string; days: DayData[]; summary: { totalGuardsNeeded: number; totalGuardsAssigned: number; totalGuardsMissing: number; }; } export default function GeneralPlanning() { const [, navigate] = useLocation(); const { toast } = useToast(); const [selectedLocation, setSelectedLocation] = useState("roccapiemonte"); const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date(), { weekStartsOn: 1 })); const [selectedCell, setSelectedCell] = useState<{ siteId: string; siteName: string; date: string; data: SiteData } | null>(null); // Form state per creazione turno const [selectedGuardId, setSelectedGuardId] = useState(""); const [days, setDays] = useState(1); // Query per dati planning settimanale const { data: planningData, isLoading } = useQuery({ queryKey: ["/api/general-planning", format(weekStart, "yyyy-MM-dd"), selectedLocation], queryFn: async () => { const response = await fetch( `/api/general-planning?weekStart=${format(weekStart, "yyyy-MM-dd")}&location=${selectedLocation}` ); if (!response.ok) throw new Error("Failed to fetch general planning"); return response.json(); }, }); // Query per guardie disponibili (solo quando dialog è aperto) const { data: availableGuards, isLoading: isLoadingGuards } = useQuery({ queryKey: ["/api/guards/availability", format(weekStart, "yyyy-MM-dd"), selectedCell?.siteId, selectedLocation], queryFn: async () => { if (!selectedCell) return []; const response = await fetch( `/api/guards/availability?weekStart=${format(weekStart, "yyyy-MM-dd")}&siteId=${selectedCell.siteId}&location=${selectedLocation}` ); if (!response.ok) throw new Error("Failed to fetch guards availability"); return response.json(); }, enabled: !!selectedCell, // Query attiva solo se dialog è aperto }); // Mutation per creare turno multi-giorno const createShiftMutation = useMutation({ mutationFn: async (data: { siteId: string; startDate: string; days: number; guardId: string }) => { return apiRequest("POST", "/api/general-planning/shifts", data); }, onSuccess: () => { // Invalida cache planning generale queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] }); queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] }); toast({ title: "Turno creato", description: "Il turno è stato creato con successo", }); // Reset form e chiudi dialog setSelectedGuardId(""); setDays(1); setSelectedCell(null); }, onError: (error: any) => { toast({ title: "Errore", description: error.message || "Impossibile creare il turno", variant: "destructive", }); }, }); // Handler per submit form creazione turno const handleCreateShift = () => { if (!selectedCell || !selectedGuardId) return; createShiftMutation.mutate({ siteId: selectedCell.siteId, startDate: selectedCell.date, days, guardId: selectedGuardId, }); }; // Navigazione settimana const goToPreviousWeek = () => setWeekStart(addWeeks(weekStart, -1)); const goToNextWeek = () => setWeekStart(addWeeks(weekStart, 1)); const goToCurrentWeek = () => setWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 })); // Formatta nome sede const formatLocation = (loc: string) => { const locations: Record = { roccapiemonte: "Roccapiemonte", milano: "Milano", roma: "Roma", }; return locations[loc] || loc; }; // Raggruppa siti unici da tutti i giorni const allSites = planningData?.days.flatMap(day => day.sites) || []; const uniqueSites = Array.from( new Map(allSites.map(site => [site.siteId, site])).values() ); // Handler per aprire dialog cella const handleCellClick = (siteId: string, siteName: string, date: string, data: SiteData) => { setSelectedCell({ siteId, siteName, date, data }); }; // Naviga a pianificazione operativa con parametri const navigateToOperationalPlanning = () => { if (selectedCell) { // Encode parameters nella URL navigate(`/operational-planning?date=${selectedCell.date}&location=${selectedLocation}`); } }; return (
{/* Header */}

Planning Generale

Vista settimanale turni con calcolo automatico guardie mancanti

{/* Filtri e navigazione */}
{/* Selezione Sede */}
{/* Navigazione settimana */}
{/* Info settimana */} {planningData && (
{format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "} {format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })}
)}
{/* Summary Guardie Settimana */} {!isLoading && planningData?.summary && ( Riepilogo Guardie Settimana

Guardie Necessarie

{planningData.summary.totalGuardsNeeded}

Guardie Pianificate

{planningData.summary.totalGuardsAssigned}

0 ? 'bg-destructive/10 border-destructive/20' : 'bg-green-500/10 border-green-500/20'}`}>

Guardie Mancanti

0 ? 'text-destructive' : 'text-green-600 dark:text-green-500'}`}> {planningData.summary.totalGuardsMissing}

)} {/* Tabella Planning */} Planning Settimanale - {formatLocation(selectedLocation)} {isLoading ? (
) : planningData ? (
{planningData.days.map((day) => ( ))} {uniqueSites.map((site) => ( {planningData.days.map((day) => { const daySiteData = day.sites.find((s) => s.siteId === site.siteId); return ( ); })} ))}
Sito
{format(new Date(day.date), "EEEE", { locale: it })} {format(new Date(day.date), "dd/MM", { locale: it })}
{site.siteName} {site.serviceType}
daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)} > {daySiteData ? (
{/* Riepilogo guardie necessarie/assegnate/mancanti - SEMPRE VISIBILE */}
{daySiteData.missingGuards > 0 ? ( Mancano {daySiteData.missingGuards} {daySiteData.missingGuards === 1 ? "guardia" : "guardie"} ) : ( Copertura Completa )}
{daySiteData.guardsAssigned + daySiteData.missingGuards} necessarie · {daySiteData.guardsAssigned} assegnate
{/* Guardie assegnate */} {daySiteData.guards.length > 0 && (
Guardie:
{daySiteData.guards.map((guard, idx) => (
{guard.badgeNumber} {guard.hours}h
))}
)} {/* Veicoli */} {daySiteData.vehicles.length > 0 && (
Veicoli:
{daySiteData.vehicles.map((vehicle, idx) => (
{vehicle.licensePlate}
))}
)} {/* Info copertura - mostra solo se ci sono turni */} {daySiteData.shiftsCount > 0 && (
Turni: {daySiteData.shiftsCount}
Tot. ore: {daySiteData.totalShiftHours}h
)}
) : (
-
)}
{uniqueSites.length === 0 && (

Nessun sito attivo per la sede selezionata

)}
) : (

Errore nel caricamento dei dati

)}
{/* Dialog dettagli cella */} setSelectedCell(null)}> {selectedCell?.siteName} {selectedCell && format(new Date(selectedCell.date), "EEEE dd MMMM yyyy", { locale: it })} {selectedCell && (
{/* Info turni */}

Turni Pianificati

{selectedCell.data.shiftsCount}

Ore Totali

{selectedCell.data.totalShiftHours}h

{/* Guardie assegnate */} {selectedCell.data.guards.length > 0 && (
Guardie Assegnate ({selectedCell.data.guards.length})
{selectedCell.data.guards.map((guard, idx) => (

{guard.guardName}

{guard.badgeNumber}

{guard.hours}h
))}
)} {/* Veicoli */} {selectedCell.data.vehicles.length > 0 && (
Veicoli Assegnati ({selectedCell.data.vehicles.length})
{selectedCell.data.vehicles.map((vehicle, idx) => (

{vehicle.licensePlate}

{vehicle.brand} {vehicle.model}

))}
)} {/* Guardie mancanti */} {selectedCell.data.missingGuards > 0 && (
Attenzione: Guardie Mancanti

Servono ancora {selectedCell.data.missingGuards}{" "} {selectedCell.data.missingGuards === 1 ? "guardia" : "guardie"} per coprire completamente il servizio (calcolato su {selectedCell.data.totalShiftHours}h con max 9h per guardia e {selectedCell.data.minGuards} {selectedCell.data.minGuards === 1 ? "guardia minima" : "guardie minime"} contemporanee)

)} {/* No turni */} {selectedCell.data.shiftsCount === 0 && (

Nessun turno pianificato per questa data

)} {/* Form creazione nuovo turno */}
Crea Nuovo Turno
{/* Select guardia disponibile */}
{isLoadingGuards ? ( ) : ( )} {availableGuards && availableGuards.length > 0 && selectedGuardId && (

{(() => { const guard = availableGuards.find(g => g.guardId === selectedGuardId); return guard ? `Ore assegnate: ${guard.weeklyHoursAssigned}h / ${guard.weeklyHoursMax}h (rimangono ${guard.weeklyHoursRemaining}h)` : ""; })()}

)}
{/* Input numero giorni */}
setDays(Math.max(1, Math.min(7, parseInt(e.target.value) || 1)))} disabled={createShiftMutation.isPending} data-testid="input-days" />

Il turno verrà creato a partire da {selectedCell && format(new Date(selectedCell.date), "dd/MM/yyyy")} per {days} {days === 1 ? "giorno" : "giorni"}

{/* Bottone crea turno */}
)}
); }