Add multi-location support for operational planning
Introduce location filtering to operational planning, site management, and resource availability queries. This includes backend route modifications to handle location parameters and frontend updates for location selection and display. 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/5GnGQQ0
This commit is contained in:
parent
121206a492
commit
eb2ccab920
@ -9,7 +9,8 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Calendar, AlertCircle, CheckCircle2, Clock, MapPin, Users, Shield, Car as CarIcon } from "lucide-react";
|
import { Calendar, AlertCircle, CheckCircle2, Clock, MapPin, Users, Shield, Car as CarIcon, Building2 } from "lucide-react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@ -84,8 +85,15 @@ interface ResourcesData {
|
|||||||
guards: Guard[];
|
guards: Guard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locationLabels: Record<string, string> = {
|
||||||
|
roccapiemonte: "Roccapiemonte",
|
||||||
|
milano: "Milano",
|
||||||
|
roma: "Roma",
|
||||||
|
};
|
||||||
|
|
||||||
export default function OperationalPlanning() {
|
export default function OperationalPlanning() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte"); // Sede selezionata (primo step)
|
||||||
const [selectedDate, setSelectedDate] = useState<string>(
|
const [selectedDate, setSelectedDate] = useState<string>(
|
||||||
format(new Date(), "yyyy-MM-dd")
|
format(new Date(), "yyyy-MM-dd")
|
||||||
);
|
);
|
||||||
@ -94,18 +102,41 @@ export default function OperationalPlanning() {
|
|||||||
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
|
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
|
||||||
const [createShiftDialogOpen, setCreateShiftDialogOpen] = useState(false);
|
const [createShiftDialogOpen, setCreateShiftDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Query per siti non coperti
|
// Query per siti non coperti (filtrati per sede e data)
|
||||||
const { data: uncoveredData, isLoading } = useQuery<UncoveredSitesData>({
|
const { data: uncoveredData, isLoading } = useQuery<UncoveredSitesData>({
|
||||||
queryKey: [`/api/operational-planning/uncovered-sites?date=${selectedDate}`, selectedDate],
|
queryKey: ['/api/operational-planning/uncovered-sites', selectedDate, selectedLocation],
|
||||||
enabled: !!selectedDate,
|
queryFn: async ({ queryKey }) => {
|
||||||
|
const [, date, location] = queryKey;
|
||||||
|
const res = await fetch(`/api/operational-planning/uncovered-sites?date=${date}&location=${location}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedDate && !!selectedLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query per risorse (veicoli e guardie) - solo quando c'è un sito selezionato
|
// Query per risorse (veicoli e guardie) - solo quando c'è un sito selezionato
|
||||||
const { data: resourcesData, isLoading: isLoadingResources } = useQuery<ResourcesData>({
|
const { data: resourcesData, isLoading: isLoadingResources } = useQuery<ResourcesData>({
|
||||||
queryKey: [`/api/operational-planning/availability?date=${selectedDate}`, selectedDate, selectedSite?.id],
|
queryKey: ['/api/operational-planning/availability', selectedDate, selectedLocation, selectedSite?.id],
|
||||||
enabled: !!selectedDate && !!selectedSite,
|
queryFn: async ({ queryKey }) => {
|
||||||
|
const [, date, location] = queryKey;
|
||||||
|
const res = await fetch(`/api/operational-planning/availability?date=${date}&location=${location}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedDate && !!selectedLocation && !!selectedSite,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleLocationChange = (location: string) => {
|
||||||
|
setSelectedLocation(location);
|
||||||
|
setSelectedSite(null);
|
||||||
|
setSelectedGuards([]);
|
||||||
|
setSelectedVehicle(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSelectedDate(e.target.value);
|
setSelectedDate(e.target.value);
|
||||||
setSelectedSite(null);
|
setSelectedSite(null);
|
||||||
@ -226,11 +257,25 @@ export default function OperationalPlanning() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Calendar className="h-5 w-5" />
|
<Calendar className="h-5 w-5" />
|
||||||
Seleziona Data
|
Seleziona Sede e Data
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex gap-4 items-end">
|
<div className="flex gap-4 items-end">
|
||||||
|
<div className="flex-1 max-w-xs">
|
||||||
|
<Label htmlFor="planning-location">Sede</Label>
|
||||||
|
<Select value={selectedLocation} onValueChange={handleLocationChange}>
|
||||||
|
<SelectTrigger id="planning-location" data-testid="select-planning-location" className="mt-1">
|
||||||
|
<Building2 className="h-4 w-4 mr-2" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||||
|
<SelectItem value="milano">Milano</SelectItem>
|
||||||
|
<SelectItem value="roma">Roma</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div className="flex-1 max-w-xs">
|
<div className="flex-1 max-w-xs">
|
||||||
<Label htmlFor="planning-date">Data</Label>
|
<Label htmlFor="planning-date">Data</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { insertSiteSchema } from "@shared/schema";
|
import { insertSiteSchema } from "@shared/schema";
|
||||||
import { Plus, MapPin, Shield, Users, Pencil } from "lucide-react";
|
import { Plus, MapPin, Shield, Users, Pencil, Building2 } from "lucide-react";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { StatusBadge } from "@/components/status-badge";
|
import { StatusBadge } from "@/components/status-badge";
|
||||||
@ -25,6 +25,12 @@ const shiftTypeLabels: Record<string, string> = {
|
|||||||
quick_response: "Pronto Intervento",
|
quick_response: "Pronto Intervento",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const locationLabels: Record<string, string> = {
|
||||||
|
roccapiemonte: "Roccapiemonte",
|
||||||
|
milano: "Milano",
|
||||||
|
roma: "Roma",
|
||||||
|
};
|
||||||
|
|
||||||
export default function Sites() {
|
export default function Sites() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
@ -129,6 +135,7 @@ export default function Sites() {
|
|||||||
editForm.reset({
|
editForm.reset({
|
||||||
name: site.name,
|
name: site.name,
|
||||||
address: site.address,
|
address: site.address,
|
||||||
|
location: site.location,
|
||||||
shiftType: site.shiftType,
|
shiftType: site.shiftType,
|
||||||
minGuards: site.minGuards,
|
minGuards: site.minGuards,
|
||||||
requiresArmed: site.requiresArmed,
|
requiresArmed: site.requiresArmed,
|
||||||
@ -221,6 +228,29 @@ export default function Sites() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="location"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Sede Gestionale</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value || "roccapiemonte"}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger data-testid="select-location">
|
||||||
|
<SelectValue placeholder="Seleziona sede" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||||
|
<SelectItem value="milano">Milano</SelectItem>
|
||||||
|
<SelectItem value="roma">Roma</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="border-t pt-4 space-y-4">
|
<div className="border-t pt-4 space-y-4">
|
||||||
<p className="text-sm font-medium">Dati Contrattuali</p>
|
<p className="text-sm font-medium">Dati Contrattuali</p>
|
||||||
|
|
||||||
@ -459,6 +489,29 @@ export default function Sites() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={editForm.control}
|
||||||
|
name="location"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Sede Gestionale</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value || "roccapiemonte"}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger data-testid="select-edit-location">
|
||||||
|
<SelectValue placeholder="Seleziona sede" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||||
|
<SelectItem value="milano">Milano</SelectItem>
|
||||||
|
<SelectItem value="roma">Roma</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="border-t pt-4 space-y-4">
|
<div className="border-t pt-4 space-y-4">
|
||||||
<p className="text-sm font-medium">Dati Contrattuali</p>
|
<p className="text-sm font-medium">Dati Contrattuali</p>
|
||||||
|
|
||||||
@ -684,9 +737,15 @@ export default function Sites() {
|
|||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<CardTitle className="text-lg truncate">{site.name}</CardTitle>
|
<CardTitle className="text-lg truncate">{site.name}</CardTitle>
|
||||||
<CardDescription className="text-xs mt-1">
|
<CardDescription className="text-xs mt-1 space-y-0.5">
|
||||||
<MapPin className="h-3 w-3 inline mr-1" />
|
<div>
|
||||||
{site.address}
|
<MapPin className="h-3 w-3 inline mr-1" />
|
||||||
|
{site.address}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Building2 className="h-3 w-3 inline mr-1" />
|
||||||
|
Sede: {locationLabels[site.location]}
|
||||||
|
</div>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
27
replit.md
27
replit.md
@ -35,11 +35,23 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
|
|||||||
- Sites include service schedule fields: `serviceStartTime` and `serviceEndTime` (formato HH:MM)
|
- Sites include service schedule fields: `serviceStartTime` and `serviceEndTime` (formato HH:MM)
|
||||||
- **Contract Management**: Sites now include contract fields: `contractReference` (codice contratto), `contractStartDate`, `contractEndDate` (date validità contratto in formato YYYY-MM-DD)
|
- **Contract Management**: Sites now include contract fields: `contractReference` (codice contratto), `contractStartDate`, `contractEndDate` (date validità contratto in formato YYYY-MM-DD)
|
||||||
- Sites now reference service types via `serviceTypeId` foreign key; `shiftType` is optional and can be derived from service type
|
- Sites now reference service types via `serviceTypeId` foreign key; `shiftType` is optional and can be derived from service type
|
||||||
|
- **Multi-Location Support**: Added `location` field (enum: roccapiemonte, milano, roma) to `sites`, `guards`, and `vehicles` tables for complete multi-sede resource isolation
|
||||||
|
|
||||||
|
**Recent Features (October 17, 2025)**:
|
||||||
|
- **Multi-Sede Operational Planning**: Redesigned operational planning workflow with location-first approach:
|
||||||
|
1. Select sede (Roccapiemonte/Milano/Roma) - first step with default value
|
||||||
|
2. Select date
|
||||||
|
3. View uncovered sites filtered by selected sede
|
||||||
|
4. Select site → view available resources (guards and vehicles) filtered by sede
|
||||||
|
5. Assign resources and create shift
|
||||||
|
- **Location-Based Filtering**: Backend endpoints use INNER JOIN with sites table to ensure complete resource isolation between locations - guards/vehicles in one sede remain available even when assigned to shifts in other sedi
|
||||||
|
- **Site Management**: Added sede selection in site creation/editing forms with visual badges showing location in site listings
|
||||||
|
|
||||||
**Recent Bug Fixes (October 17, 2025)**:
|
**Recent Bug Fixes (October 17, 2025)**:
|
||||||
- **Operational Planning Date Handling**: Fixed date sanitization in `/api/operational-planning/uncovered-sites` and `/api/operational-planning/availability` endpoints to handle malformed date inputs (e.g., "2025-10-17/2025-10-17"). Both endpoints now validate dates using `parseISO`/`isValid` and return 400 for invalid formats.
|
- **Operational Planning Date Handling**: Fixed date sanitization in `/api/operational-planning/uncovered-sites` and `/api/operational-planning/availability` endpoints to handle malformed date inputs (e.g., "2025-10-17/2025-10-17"). Both endpoints now validate dates using `parseISO`/`isValid` and return 400 for invalid formats.
|
||||||
- **Checkbox Event Propagation**: Fixed double-toggle bug in operational planning resource selection by wrapping vehicle and guard checkboxes in `<div onClick={e => e.stopPropagation()}>` to prevent Card onClick from firing when clicking checkboxes.
|
- **Checkbox Event Propagation**: Fixed double-toggle bug in operational planning resource selection by wrapping vehicle and guard checkboxes in `<div onClick={e => e.stopPropagation()}>` to prevent Card onClick from firing when clicking checkboxes.
|
||||||
- **Resource Query Key**: Added `selectedSite?.id` to TanStack Query queryKey for availability endpoint to ensure resources re-fetch when operator selects a different site.
|
- **Multi-Sede Resource Isolation**: Fixed critical bug where resources from different sedi were incorrectly marked as unavailable due to global shift queries. Now both availability and uncovered-sites endpoints filter shifts by location using JOIN with sites table.
|
||||||
|
- **QueryKey Cache Invalidation**: Fixed queryKey structure from single-string to hierarchical array with custom queryFn to enable targeted cache invalidation by location and date while preventing URL concatenation errors.
|
||||||
|
|
||||||
### API Endpoints
|
### API Endpoints
|
||||||
Comprehensive RESTful API endpoints are provided for Authentication, Users, Guards, Sites, Shifts, and Notifications, supporting full CRUD operations with role-based access control.
|
Comprehensive RESTful API endpoints are provided for Authentication, Users, Guards, Sites, Shifts, and Notifications, supporting full CRUD operations with role-based access control.
|
||||||
@ -56,11 +68,12 @@ Key frontend routes include `/`, `/guards`, `/sites`, `/shifts`, `/reports`, `/n
|
|||||||
### Key Features
|
### Key Features
|
||||||
- **Dashboard Operativa**: Live KPIs (active shifts, total guards, active sites, expiring certifications) and real-time shift status.
|
- **Dashboard Operativa**: Live KPIs (active shifts, total guards, active sites, expiring certifications) and real-time shift status.
|
||||||
- **Gestione Guardie**: Complete profiles with skill matrix (armed, fire safety, first aid, driver's license), certification management with automatic expiry, and unique badge numbers.
|
- **Gestione Guardie**: Complete profiles with skill matrix (armed, fire safety, first aid, driver's license), certification management with automatic expiry, and unique badge numbers.
|
||||||
- **Gestione Siti/Commesse**: Service types with specialized parameters (fixed post hours, patrol passages, inspection frequency, response time) and minimum requirements (guard count, armed, driver's license). Sites include service schedule (start/end time) and contract management (reference code, validity period with start/end dates). Contract status is visualized with badges (active/expiring/expired) and enforces shift creation only within active contract periods.
|
- **Gestione Siti/Commesse**: Service types with specialized parameters (fixed post hours, patrol passages, inspection frequency, response time) and minimum requirements (guard count, armed, driver's license). Sites include service schedule (start/end time), contract management (reference code, validity period with start/end dates), and location/sede assignment. Contract status is visualized with badges (active/expiring/expired) and enforces shift creation only within active contract periods.
|
||||||
- **Pianificazione Operativa Interattiva**: Three-step workflow for shift assignment:
|
- **Pianificazione Operativa Multi-Sede**: Location-aware workflow for shift assignment:
|
||||||
1. Select date → view uncovered sites with coverage status
|
1. Select sede (Roccapiemonte/Milano/Roma) → filters all subsequent data by location
|
||||||
2. Select site → view filtered resources (guards and vehicles matching requirements)
|
2. Select date → view uncovered sites with coverage status (sede-filtered)
|
||||||
3. Assign resources → create shift with atomic guard assignments and vehicle allocation
|
3. Select site → view available resources (guards and vehicles matching sede and requirements)
|
||||||
|
4. Assign resources → create shift with atomic guard assignments and vehicle allocation
|
||||||
- **Pianificazione Turni**: 24/7 calendar, manual guard assignment, basic constraints, and shift statuses (planned, active, completed, cancelled).
|
- **Pianificazione Turni**: 24/7 calendar, manual guard assignment, basic constraints, and shift statuses (planned, active, completed, cancelled).
|
||||||
- **Reportistica**: Total hours worked, monthly hours per guard, shift statistics, and data export capabilities.
|
- **Reportistica**: Total hours worked, monthly hours per guard, shift statistics, and data export capabilities.
|
||||||
- **Advanced Planning**: Management of guard constraints (preferences, max hours, rest days), site preferences (preferred/blacklisted guards), contract parameters, training courses, holidays, and absences with substitution system.
|
- **Advanced Planning**: Management of guard constraints (preferences, max hours, rest days), site preferences (preferred/blacklisted guards), contract parameters, training courses, holidays, and absences with substitution system.
|
||||||
@ -77,4 +90,4 @@ Key frontend routes include `/`, `/guards`, `/sites`, `/shifts`, `/reports`, `/n
|
|||||||
- **PM2**: Production process manager for Node.js applications.
|
- **PM2**: Production process manager for Node.js applications.
|
||||||
- **Nginx**: As a reverse proxy for the production environment.
|
- **Nginx**: As a reverse proxy for the production environment.
|
||||||
- **Let's Encrypt**: For SSL/TLS certificates.
|
- **Let's Encrypt**: For SSL/TLS certificates.
|
||||||
- **GitLab CI/CD**: For continuous integration and deployment.
|
- **GitLab CI/CD**: For continuous integration and deployment.
|
||||||
|
|||||||
@ -551,60 +551,71 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
const dateStr = format(parsedDate, "yyyy-MM-dd");
|
const dateStr = format(parsedDate, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Ottieni location dalla query (default: roccapiemonte)
|
||||||
|
const location = req.query.location as string || "roccapiemonte";
|
||||||
|
|
||||||
// Imposta inizio e fine giornata in UTC
|
// Imposta inizio e fine giornata in UTC
|
||||||
const startOfDay = new Date(dateStr + "T00:00:00.000Z");
|
const startOfDay = new Date(dateStr + "T00:00:00.000Z");
|
||||||
const endOfDay = new Date(dateStr + "T23:59:59.999Z");
|
const endOfDay = new Date(dateStr + "T23:59:59.999Z");
|
||||||
|
|
||||||
// Ottieni tutti i veicoli
|
// Ottieni tutti i veicoli della sede selezionata
|
||||||
const allVehicles = await storage.getAllVehicles();
|
const allVehicles = await storage.getAllVehicles();
|
||||||
|
const locationVehicles = allVehicles.filter(v => v.location === location);
|
||||||
|
|
||||||
// Ottieni turni del giorno per trovare veicoli assegnati
|
// Ottieni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location)
|
||||||
const dayShifts = await db
|
const dayShifts = await db
|
||||||
.select()
|
.select({
|
||||||
|
shift: shifts
|
||||||
|
})
|
||||||
.from(shifts)
|
.from(shifts)
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
gte(shifts.startTime, startOfDay),
|
gte(shifts.startTime, startOfDay),
|
||||||
lte(shifts.startTime, endOfDay)
|
lte(shifts.startTime, endOfDay),
|
||||||
|
eq(sites.location, location)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mappa veicoli con disponibilità
|
// Mappa veicoli con disponibilità
|
||||||
const vehiclesWithAvailability = await Promise.all(
|
const vehiclesWithAvailability = await Promise.all(
|
||||||
allVehicles.map(async (vehicle) => {
|
locationVehicles.map(async (vehicle) => {
|
||||||
const assignedShift = dayShifts.find((shift: any) => shift.vehicleId === vehicle.id);
|
const assignedShiftRecord = dayShifts.find((s: any) => s.shift.vehicleId === vehicle.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...vehicle,
|
...vehicle,
|
||||||
isAvailable: !assignedShift,
|
isAvailable: !assignedShiftRecord,
|
||||||
assignedShift: assignedShift ? {
|
assignedShift: assignedShiftRecord ? {
|
||||||
id: assignedShift.id,
|
id: assignedShiftRecord.shift.id,
|
||||||
startTime: assignedShift.startTime,
|
startTime: assignedShiftRecord.shift.startTime,
|
||||||
endTime: assignedShift.endTime,
|
endTime: assignedShiftRecord.shift.endTime,
|
||||||
siteId: assignedShift.siteId
|
siteId: assignedShiftRecord.shift.siteId
|
||||||
} : null
|
} : null
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ottieni tutte le guardie
|
// Ottieni tutte le guardie della sede selezionata
|
||||||
const allGuards = await storage.getAllGuards();
|
const allGuards = await storage.getAllGuards();
|
||||||
|
const locationGuards = allGuards.filter(g => g.location === location);
|
||||||
|
|
||||||
// Ottieni assegnazioni turni del giorno
|
// Ottieni assegnazioni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location)
|
||||||
const dayShiftAssignments = await db
|
const dayShiftAssignments = await db
|
||||||
.select()
|
.select()
|
||||||
.from(shiftAssignments)
|
.from(shiftAssignments)
|
||||||
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
gte(shifts.startTime, startOfDay),
|
gte(shifts.startTime, startOfDay),
|
||||||
lte(shifts.startTime, endOfDay)
|
lte(shifts.startTime, endOfDay),
|
||||||
|
eq(sites.location, location)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calcola disponibilità agenti con report CCNL
|
// Calcola disponibilità agenti con report CCNL
|
||||||
const guardsWithAvailability = await Promise.all(
|
const guardsWithAvailability = await Promise.all(
|
||||||
allGuards.map(async (guard) => {
|
locationGuards.map(async (guard) => {
|
||||||
const assignedShift = dayShiftAssignments.find(
|
const assignedShift = dayShiftAssignments.find(
|
||||||
(assignment: any) => assignment.shift_assignments.guardId === guard.id
|
(assignment: any) => assignment.shift_assignments.guardId === guard.id
|
||||||
);
|
);
|
||||||
@ -664,7 +675,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Endpoint per ottenere siti non completamente coperti per una data
|
// Endpoint per ottenere siti non completamente coperti per una data e sede
|
||||||
app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => {
|
app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17"
|
// Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17"
|
||||||
@ -679,11 +690,19 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
const dateStr = format(parsedDate, "yyyy-MM-dd");
|
const dateStr = format(parsedDate, "yyyy-MM-dd");
|
||||||
|
|
||||||
// Ottieni tutti i siti attivi
|
// Ottieni location dalla query (default: roccapiemonte)
|
||||||
|
const location = req.query.location as string || "roccapiemonte";
|
||||||
|
|
||||||
|
// Ottieni tutti i siti attivi della sede selezionata
|
||||||
const allSites = await db
|
const allSites = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.isActive, true));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.isActive, true),
|
||||||
|
eq(sites.location, location)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Filtra siti con contratto valido per la data selezionata
|
// Filtra siti con contratto valido per la data selezionata
|
||||||
const sitesWithValidContract = allSites.filter((site: any) => {
|
const sitesWithValidContract = allSites.filter((site: any) => {
|
||||||
@ -706,7 +725,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
return selectedDate >= contractStart && selectedDate <= contractEnd;
|
return selectedDate >= contractStart && selectedDate <= contractEnd;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ottieni turni del giorno con assegnazioni
|
// Ottieni turni del giorno con assegnazioni SOLO della sede selezionata
|
||||||
const startOfDayDate = new Date(dateStr);
|
const startOfDayDate = new Date(dateStr);
|
||||||
startOfDayDate.setHours(0, 0, 0, 0);
|
startOfDayDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@ -719,12 +738,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
assignmentCount: sql<number>`count(${shiftAssignments.id})::int`
|
assignmentCount: sql<number>`count(${shiftAssignments.id})::int`
|
||||||
})
|
})
|
||||||
.from(shifts)
|
.from(shifts)
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
.leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId))
|
.leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
gte(shifts.startTime, startOfDayDate),
|
gte(shifts.startTime, startOfDayDate),
|
||||||
lte(shifts.startTime, endOfDayDate),
|
lte(shifts.startTime, endOfDayDate),
|
||||||
ne(shifts.status, "cancelled")
|
ne(shifts.status, "cancelled"),
|
||||||
|
eq(sites.location, location)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.groupBy(shifts.id);
|
.groupBy(shifts.id);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user