Add ability to view and edit shift details for each location
Integrates a dialog modal for detailed shift information, enabling navigation to operational planning and allowing users to view and potentially edit shift assignments. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/uZXH8P1
This commit is contained in:
parent
10a113c4a7
commit
fad541525b
@ -2,12 +2,21 @@ import { useState } from "react";
|
||||
import { useQuery } 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 { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit } 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";
|
||||
|
||||
interface GuardWithHours {
|
||||
guardId: string;
|
||||
@ -50,8 +59,10 @@ interface GeneralPlanningResponse {
|
||||
}
|
||||
|
||||
export default function GeneralPlanning() {
|
||||
const [, navigate] = useLocation();
|
||||
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte");
|
||||
const [weekStart, setWeekStart] = useState<Date>(() => startOfWeek(new Date(), { weekStartsOn: 1 }));
|
||||
const [selectedCell, setSelectedCell] = useState<{ siteId: string; siteName: string; date: string; data: SiteData } | null>(null);
|
||||
|
||||
// Query per dati planning settimanale
|
||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||
@ -86,6 +97,19 @@ export default function GeneralPlanning() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@ -219,6 +243,7 @@ export default function GeneralPlanning() {
|
||||
key={day.date}
|
||||
className="p-2 border-r hover:bg-accent/5 cursor-pointer"
|
||||
data-testid={`cell-${site.siteId}-${day.date}`}
|
||||
onClick={() => daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)}
|
||||
>
|
||||
{daySiteData && daySiteData.shiftsCount > 0 ? (
|
||||
<div className="space-y-2 text-xs">
|
||||
@ -298,6 +323,111 @@ export default function GeneralPlanning() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dialog dettagli cella */}
|
||||
<Dialog open={!!selectedCell} onOpenChange={() => setSelectedCell(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
{selectedCell?.siteName}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedCell && format(new Date(selectedCell.date), "EEEE dd MMMM yyyy", { locale: it })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedCell && (
|
||||
<div className="space-y-4">
|
||||
{/* Info turni */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Turni Pianificati</p>
|
||||
<p className="text-2xl font-bold">{selectedCell.data.shiftsCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Ore Totali</p>
|
||||
<p className="text-2xl font-bold">{selectedCell.data.totalShiftHours}h</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guardie assegnate */}
|
||||
{selectedCell.data.guards.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Users className="h-4 w-4" />
|
||||
Guardie Assegnate ({selectedCell.data.guards.length})
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{selectedCell.data.guards.map((guard, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between p-2 bg-accent/10 rounded-md">
|
||||
<div>
|
||||
<p className="font-medium">{guard.guardName}</p>
|
||||
<p className="text-xs text-muted-foreground">{guard.badgeNumber}</p>
|
||||
</div>
|
||||
<Badge variant="secondary">{guard.hours}h</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Veicoli */}
|
||||
{selectedCell.data.vehicles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Car className="h-4 w-4" />
|
||||
Veicoli Assegnati ({selectedCell.data.vehicles.length})
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{selectedCell.data.vehicles.map((vehicle, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 p-2 bg-accent/10 rounded-md">
|
||||
<p className="font-medium">{vehicle.licensePlate}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{vehicle.brand} {vehicle.model}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Guardie mancanti */}
|
||||
{selectedCell.data.missingGuards > 0 && (
|
||||
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<div className="flex items-center gap-2 text-destructive font-semibold mb-2">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Attenzione: Guardie Mancanti
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
Servono ancora <span className="font-bold">{selectedCell.data.missingGuards}</span>{" "}
|
||||
{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)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No turni */}
|
||||
{selectedCell.data.shiftsCount === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Nessun turno pianificato per questa data</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSelectedCell(null)}>
|
||||
Chiudi
|
||||
</Button>
|
||||
<Button onClick={navigateToOperationalPlanning} data-testid="button-edit-planning">
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Modifica in Pianificazione Operativa
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user