Compare commits
7 Commits
d8a6ec9c49
...
1598eb208b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1598eb208b | ||
|
|
8bb0386d1e | ||
|
|
ba0bd4d36f | ||
|
|
9c28befcb1 | ||
|
|
fb99b5f738 | ||
|
|
153f272c15 | ||
|
|
3b7c55b55b |
BIN
attached_assets/immagine_1761123543970.png
Normal file
BIN
attached_assets/immagine_1761123543970.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@ -56,7 +56,7 @@ const menuItems = [
|
|||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Planning Generale",
|
title: "Planning Fissi",
|
||||||
url: "/general-planning",
|
url: "/general-planning",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { format, startOfWeek, addWeeks } from "date-fns";
|
import { format, startOfWeek, addWeeks } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
@ -19,6 +19,16 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
import { queryClient, apiRequest } from "@/lib/queryClient";
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { GuardAvailability, Vehicle as VehicleDb } from "@shared/schema";
|
import type { GuardAvailability, Vehicle as VehicleDb } from "@shared/schema";
|
||||||
@ -44,6 +54,9 @@ interface SiteData {
|
|||||||
siteId: string;
|
siteId: string;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
serviceType: string;
|
serviceType: string;
|
||||||
|
serviceStartTime: string;
|
||||||
|
serviceEndTime: string;
|
||||||
|
serviceHours: number;
|
||||||
minGuards: number;
|
minGuards: number;
|
||||||
guards: GuardWithHours[];
|
guards: GuardWithHours[];
|
||||||
vehicles: Vehicle[];
|
vehicles: Vehicle[];
|
||||||
@ -103,6 +116,7 @@ export default function GeneralPlanning() {
|
|||||||
const [durationHours, setDurationHours] = useState<number>(8);
|
const [durationHours, setDurationHours] = useState<number>(8);
|
||||||
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
||||||
const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
|
const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
|
||||||
|
const [ccnlConfirmation, setCcnlConfirmation] = useState<{ message: string; data: any } | null>(null);
|
||||||
|
|
||||||
// Query per dati planning settimanale
|
// Query per dati planning settimanale
|
||||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||||
@ -161,14 +175,28 @@ export default function GeneralPlanning() {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calcola dati aggiornati della cella selezionata (per auto-refresh dialog)
|
||||||
|
const currentCellData = (() => {
|
||||||
|
if (!selectedCell || !planningData) return selectedCell?.data;
|
||||||
|
|
||||||
|
// Trova i dati freschi da planningData
|
||||||
|
const day = planningData.days.find(d => d.date === selectedCell.date);
|
||||||
|
if (!day) return selectedCell.data;
|
||||||
|
|
||||||
|
const updatedSite = day.sites.find(s => s.siteId === selectedCell.siteId);
|
||||||
|
return updatedSite || selectedCell.data;
|
||||||
|
})();
|
||||||
|
|
||||||
// Mutation per eliminare assegnazione guardia
|
// Mutation per eliminare assegnazione guardia
|
||||||
const deleteAssignmentMutation = useMutation({
|
const deleteAssignmentMutation = useMutation({
|
||||||
mutationFn: async (assignmentId: string) => {
|
mutationFn: async (assignmentId: string) => {
|
||||||
return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, undefined);
|
return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, undefined);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
|
// Invalida e refetch planning generale per aggiornare dialog
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
|
await queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
|
||||||
|
await queryClient.refetchQueries({ queryKey: ["/api/general-planning"] });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Guardia rimossa",
|
title: "Guardia rimossa",
|
||||||
@ -186,7 +214,7 @@ export default function GeneralPlanning() {
|
|||||||
|
|
||||||
// Mutation per assegnare guardia con orari (anche multi-giorno)
|
// Mutation per assegnare guardia con orari (anche multi-giorno)
|
||||||
const assignGuardMutation = useMutation({
|
const assignGuardMutation = useMutation({
|
||||||
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number; vehicleId?: string }) => {
|
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number; vehicleId?: string; force?: boolean }) => {
|
||||||
return apiRequest("POST", "/api/general-planning/assign-guard", data);
|
return apiRequest("POST", "/api/general-planning/assign-guard", data);
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@ -206,19 +234,33 @@ export default function GeneralPlanning() {
|
|||||||
// Reset form (NON chiudere dialog per vedere lista aggiornata)
|
// Reset form (NON chiudere dialog per vedere lista aggiornata)
|
||||||
setSelectedGuardId("");
|
setSelectedGuardId("");
|
||||||
setSelectedVehicleId("");
|
setSelectedVehicleId("");
|
||||||
|
setCcnlConfirmation(null); // Reset dialog conferma se aperto
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any, variables) => {
|
||||||
// Parse error message from API response
|
// Parse error message from API response
|
||||||
let errorMessage = "Impossibile assegnare la guardia";
|
let errorMessage = "Impossibile assegnare la guardia";
|
||||||
|
let errorType = "";
|
||||||
|
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
// Error format from apiRequest: "STATUS_CODE: {json_body}"
|
// Error format from apiRequest: "STATUS_CODE: {json_body}"
|
||||||
const match = error.message.match(/^\d+:\s*(.+)$/);
|
const match = error.message.match(/^(\d+):\s*(.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const statusCode = match[1];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(match[1]);
|
const parsed = JSON.parse(match[2]);
|
||||||
errorMessage = parsed.message || errorMessage;
|
errorMessage = parsed.message || errorMessage;
|
||||||
|
errorType = parsed.type || "";
|
||||||
|
|
||||||
|
// Se è un errore CCNL (409 con tipo CCNL_VIOLATION), mostra dialog conferma
|
||||||
|
if (statusCode === "409" && errorType === "CCNL_VIOLATION") {
|
||||||
|
setCcnlConfirmation({
|
||||||
|
message: errorMessage,
|
||||||
|
data: variables
|
||||||
|
});
|
||||||
|
return; // Non mostrare toast, mostra dialog
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = match[1];
|
errorMessage = match[2];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
@ -288,7 +330,7 @@ export default function GeneralPlanning() {
|
|||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight" data-testid="text-page-title">
|
<h1 className="text-3xl font-bold tracking-tight" data-testid="text-page-title">
|
||||||
Planning Generale
|
Planning Fissi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Vista settimanale turni con calcolo automatico guardie mancanti
|
Vista settimanale turni con calcolo automatico guardie mancanti
|
||||||
@ -775,14 +817,14 @@ export default function GeneralPlanning() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Guardie già assegnate - fuori dal form box per evitare di nascondere il form */}
|
{/* Guardie già assegnate - fuori dal form box per evitare di nascondere il form */}
|
||||||
{selectedCell.data.guards.length > 0 && (
|
{currentCellData && currentCellData.guards.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
Guardie Già Assegnate ({selectedCell.data.guards.length})
|
Guardie Già Assegnate ({currentCellData.guards.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{selectedCell.data.guards.map((guard, idx) => (
|
{currentCellData.guards.map((guard, idx) => (
|
||||||
<div key={idx} className="flex items-start justify-between gap-2 bg-muted/30 p-2.5 rounded border">
|
<div key={idx} className="flex items-start justify-between gap-2 bg-muted/30 p-2.5 rounded border">
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -820,43 +862,61 @@ export default function GeneralPlanning() {
|
|||||||
|
|
||||||
{/* Info turni esistenti */}
|
{/* Info turni esistenti */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="font-semibold text-sm">Situazione Attuale</h3>
|
<h3 className="font-semibold text-sm">Informazioni Servizio</h3>
|
||||||
|
|
||||||
|
{/* Tipo servizio e orario */}
|
||||||
|
{currentCellData && (
|
||||||
|
<div className="bg-muted/30 p-3 rounded-md space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Tipo Servizio</span>
|
||||||
|
<Badge variant="outline">{currentCellData.serviceType}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Orario Servizio</span>
|
||||||
|
<span className="text-sm font-medium">{currentCellData.serviceStartTime} - {currentCellData.serviceEndTime}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Ore Richieste</span>
|
||||||
|
<span className="text-sm font-bold">{currentCellData.serviceHours}h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Turni Pianificati</p>
|
<p className="text-sm text-muted-foreground">Turni Pianificati</p>
|
||||||
<p className="text-2xl font-bold">{selectedCell.data.shiftsCount}</p>
|
<p className="text-2xl font-bold">{currentCellData?.shiftsCount || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Ore Totali</p>
|
<p className="text-sm text-muted-foreground">Ore Assegnate</p>
|
||||||
<p className="text-2xl font-bold">{selectedCell.data.totalShiftHours}h</p>
|
<p className="text-2xl font-bold">{currentCellData?.totalShiftHours || 0}h</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Guardie mancanti */}
|
{/* Guardie mancanti */}
|
||||||
{selectedCell.data.missingGuards > 0 && (
|
{currentCellData && currentCellData.missingGuards > 0 && (
|
||||||
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
<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">
|
<div className="flex items-center gap-2 text-destructive font-semibold mb-2">
|
||||||
<AlertTriangle className="h-5 w-5" />
|
<AlertTriangle className="h-5 w-5" />
|
||||||
Attenzione: Guardie Mancanti
|
Attenzione: Guardie Mancanti
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Servono ancora <span className="font-bold">{selectedCell.data.missingGuards}</span>{" "}
|
Servono ancora <span className="font-bold">{currentCellData.missingGuards}</span>{" "}
|
||||||
{selectedCell.data.missingGuards === 1 ? "guardia" : "guardie"} per coprire completamente il servizio
|
{currentCellData.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)
|
(calcolato su {currentCellData.totalShiftHours}h con max 9h per guardia e {currentCellData.minGuards} {currentCellData.minGuards === 1 ? "guardia minima" : "guardie minime"} contemporanee)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Veicoli */}
|
{/* Veicoli */}
|
||||||
{selectedCell.data.vehicles.length > 0 && (
|
{currentCellData && currentCellData.vehicles.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<Car className="h-4 w-4" />
|
<Car className="h-4 w-4" />
|
||||||
Veicoli Assegnati ({selectedCell.data.vehicles.length})
|
Veicoli Assegnati ({currentCellData.vehicles.length})
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{selectedCell.data.vehicles.map((vehicle, idx) => (
|
{currentCellData.vehicles.map((vehicle, idx) => (
|
||||||
<div key={idx} className="flex items-center gap-2 p-2 bg-accent/10 rounded-md">
|
<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="font-medium">{vehicle.licensePlate}</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@ -891,6 +951,43 @@ export default function GeneralPlanning() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Dialog conferma forzatura CCNL */}
|
||||||
|
<AlertDialog open={!!ccnlConfirmation} onOpenChange={() => setCcnlConfirmation(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-600" />
|
||||||
|
Superamento Limite CCNL
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-2">
|
||||||
|
<p className="text-foreground font-medium">
|
||||||
|
{ccnlConfirmation?.message}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Vuoi forzare comunque l'assegnazione? L'operazione verrà registrata e potrai consultarla nei report.
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel data-testid="button-cancel-force">Annulla</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => {
|
||||||
|
if (ccnlConfirmation) {
|
||||||
|
assignGuardMutation.mutate({
|
||||||
|
...ccnlConfirmation.data,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid="button-confirm-force"
|
||||||
|
className="bg-yellow-600 hover:bg-yellow-700"
|
||||||
|
>
|
||||||
|
Forza Assegnazione
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.34_20251023_080247.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.34_20251023_080247.sql.gz
Normal file
Binary file not shown.
@ -46,7 +46,7 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
|
|||||||
5. Assign resources and create shift
|
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
|
- **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
|
- **Site Management**: Added sede selection in site creation/editing forms with visual badges showing location in site listings
|
||||||
- **Planning Generale (October 18, 2025)**: New weekly planning overview feature showing all sites × 7 days in table format:
|
- **Planning Fissi (October 18, 2025)**: New weekly planning overview feature showing all sites × 7 days in table format:
|
||||||
- **Contract filtering**: Shows only sites with active contracts in the week dates (`contractStartDate <= weekEnd AND contractEndDate >= weekStart`)
|
- **Contract filtering**: Shows only sites with active contracts in the week dates (`contractStartDate <= weekEnd AND contractEndDate >= weekStart`)
|
||||||
- Backend endpoint `/api/general-planning?weekStart=YYYY-MM-DD&location=sede` with complex joins and location filtering
|
- Backend endpoint `/api/general-planning?weekStart=YYYY-MM-DD&location=sede` with complex joins and location filtering
|
||||||
- Automatic missing guards calculation: `ceil(totalShiftHours / maxHoursPerGuard) × minGuards - assignedGuards` (e.g., 24h shift, 2 guards min, 9h max = 6 total needed)
|
- Automatic missing guards calculation: `ceil(totalShiftHours / maxHoursPerGuard) × minGuards - assignedGuards` (e.g., 24h shift, 2 guards min, 9h max = 6 total needed)
|
||||||
|
|||||||
190
server/routes.ts
190
server/routes.ts
@ -4,9 +4,11 @@ import { storage } from "./storage";
|
|||||||
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
||||||
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes, createMultiDayShiftSchema } from "@shared/schema";
|
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes, createMultiDayShiftSchema, insertCustomerSchema } from "@shared/schema";
|
||||||
import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm";
|
import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm";
|
||||||
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns";
|
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
// Determina quale sistema auth usare basandosi sull'ambiente
|
// Determina quale sistema auth usare basandosi sull'ambiente
|
||||||
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
|
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
|
||||||
@ -1026,37 +1028,41 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const maxOreGuardia = 9; // Max ore per guardia
|
const maxOreGuardia = 9; // Max ore per guardia
|
||||||
const minGuardie = site.minGuards || 1;
|
const minGuardie = site.minGuards || 1;
|
||||||
|
|
||||||
// Somma ore totali dei turni del giorno
|
// Calcola ore servizio del sito (per calcolo corretto anche senza turni)
|
||||||
|
const serviceStart = site.serviceStartTime || "00:00";
|
||||||
|
const serviceEnd = site.serviceEndTime || "23:59";
|
||||||
|
const [startH, startM] = serviceStart.split(":").map(Number);
|
||||||
|
const [endH, endM] = serviceEnd.split(":").map(Number);
|
||||||
|
let serviceHours = (endH + endM/60) - (startH + startM/60);
|
||||||
|
if (serviceHours <= 0) serviceHours += 24; // Servizio notturno (es. 22:00-06:00)
|
||||||
|
|
||||||
|
// Somma ore totali dei turni del giorno (se esistono)
|
||||||
const totalShiftHours = dayShifts.reduce((sum: number, ds: any) => {
|
const totalShiftHours = dayShifts.reduce((sum: number, ds: any) => {
|
||||||
const start = new Date(ds.shift.startTime);
|
const start = new Date(ds.shift.startTime);
|
||||||
const end = new Date(ds.shift.endTime);
|
const end = new Date(ds.shift.endTime);
|
||||||
return sum + differenceInHours(end, start);
|
return sum + differenceInHours(end, start);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
// Usa ore servizio o ore turni (se già creati)
|
||||||
|
const effectiveHours = totalShiftHours > 0 ? totalShiftHours : serviceHours;
|
||||||
|
|
||||||
// Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
|
// Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
|
||||||
const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size;
|
const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size;
|
||||||
|
|
||||||
// Calcolo guardie necessarie e mancanti
|
// Calcolo guardie necessarie basato su ore servizio
|
||||||
let totalGuardsNeeded: number;
|
// Slot necessari per coprire le ore (ogni guardia max 9h)
|
||||||
let missingGuards: number;
|
const slotsNeeded = Math.ceil(effectiveHours / maxOreGuardia);
|
||||||
|
// Guardie totali necessarie (slot × min guardie contemporanee)
|
||||||
if (totalShiftHours > 0) {
|
const totalGuardsNeeded = slotsNeeded * minGuardie;
|
||||||
// Se ci sono turni: calcola basandosi sulle ore
|
const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
|
||||||
// Slot necessari per coprire le ore totali
|
|
||||||
const slotsNeeded = Math.ceil(totalShiftHours / maxOreGuardia);
|
|
||||||
// Guardie totali necessarie (slot × min guardie contemporanee)
|
|
||||||
totalGuardsNeeded = slotsNeeded * minGuardie;
|
|
||||||
missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
|
|
||||||
} else {
|
|
||||||
// Se NON ci sono turni: serve almeno la copertura minima
|
|
||||||
totalGuardsNeeded = minGuardie;
|
|
||||||
missingGuards = minGuardie; // Tutte mancanti perché non ci sono turni
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
siteId: site.id,
|
siteId: site.id,
|
||||||
siteName: site.name,
|
siteName: site.name,
|
||||||
serviceType: serviceType?.label || "N/A",
|
serviceType: serviceType?.label || "N/A",
|
||||||
|
serviceStartTime: serviceStart,
|
||||||
|
serviceEndTime: serviceEnd,
|
||||||
|
serviceHours: Math.round(serviceHours * 10) / 10, // Arrotonda a 1 decimale
|
||||||
minGuards: site.minGuards,
|
minGuards: site.minGuards,
|
||||||
guards: guardsWithHours,
|
guards: guardsWithHours,
|
||||||
vehicles: dayVehicles,
|
vehicles: dayVehicles,
|
||||||
@ -1240,7 +1246,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Assign guard to site/date with specific time slot (supports multi-day assignments)
|
// Assign guard to site/date with specific time slot (supports multi-day assignments)
|
||||||
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
|
app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { siteId, date, guardId, startTime, durationHours, consecutiveDays = 1, vehicleId } = req.body;
|
const { siteId, date, guardId, startTime, durationHours, consecutiveDays = 1, vehicleId, force = false } = req.body;
|
||||||
|
|
||||||
if (!siteId || !date || !guardId || !startTime || !durationHours) {
|
if (!siteId || !date || !guardId || !startTime || !durationHours) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@ -1287,8 +1293,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const actualMonth = baseDate.getMonth();
|
const actualMonth = baseDate.getMonth();
|
||||||
const actualDay = baseDate.getDate();
|
const actualDay = baseDate.getDate();
|
||||||
|
|
||||||
// Build dates in UTC to avoid timezone shifts
|
// Build dates in LOCAL timezone to match user's selection
|
||||||
const shiftDate = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0));
|
const shiftDate = new Date(actualYear, actualMonth, actualDay, 0, 0, 0, 0);
|
||||||
|
|
||||||
// Check contract validity for this date
|
// Check contract validity for this date
|
||||||
if (site.contractStartDate && site.contractEndDate) {
|
if (site.contractStartDate && site.contractEndDate) {
|
||||||
@ -1301,13 +1307,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate planned start and end times in UTC
|
// Calculate planned start and end times in LOCAL timezone
|
||||||
const plannedStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours, minutes, 0, 0));
|
const plannedStart = new Date(actualYear, actualMonth, actualDay, hours, minutes, 0, 0);
|
||||||
const plannedEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0));
|
const plannedEnd = new Date(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0);
|
||||||
|
|
||||||
// Find or create shift for this site/date (full day boundaries in UTC)
|
// Find or create shift for this site/date (full day boundaries in LOCAL timezone)
|
||||||
const dayStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0));
|
const dayStart = new Date(actualYear, actualMonth, actualDay, 0, 0, 0, 0);
|
||||||
const dayEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, 23, 59, 59, 999));
|
const dayEnd = new Date(actualYear, actualMonth, actualDay, 23, 59, 59, 999);
|
||||||
|
|
||||||
let existingShifts = await tx
|
let existingShifts = await tx
|
||||||
.select()
|
.select()
|
||||||
@ -1331,12 +1337,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const [startHour, startMin] = serviceStart.split(":").map(Number);
|
const [startHour, startMin] = serviceStart.split(":").map(Number);
|
||||||
const [endHour, endMin] = serviceEnd.split(":").map(Number);
|
const [endHour, endMin] = serviceEnd.split(":").map(Number);
|
||||||
|
|
||||||
const shiftStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0));
|
const shiftStart = new Date(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0);
|
||||||
let shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0));
|
let shiftEnd = new Date(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0);
|
||||||
|
|
||||||
// If end time is before/equal to start time, shift extends to next day
|
// If end time is before/equal to start time, shift extends to next day
|
||||||
if (shiftEnd <= shiftStart) {
|
if (shiftEnd <= shiftStart) {
|
||||||
shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0));
|
shiftEnd = new Date(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[shift] = await tx.insert(shifts).values({
|
[shift] = await tx.insert(shifts).values({
|
||||||
@ -1368,32 +1374,35 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CCNL: Check daily hour limit (max 9h/day)
|
// CCNL: Check daily hour limit (max 9h/day) - skip if force=true
|
||||||
const maxDailyHours = 9;
|
if (!force) {
|
||||||
let dailyHoursAlreadyAssigned = 0;
|
const maxDailyHours = 9;
|
||||||
|
let dailyHoursAlreadyAssigned = 0;
|
||||||
for (const existing of existingAssignments) {
|
|
||||||
// Check if assignment is on the same day
|
for (const existing of existingAssignments) {
|
||||||
const existingDate = new Date(existing.plannedStartTime);
|
// Check if assignment is on the same day
|
||||||
if (
|
const existingDate = new Date(existing.plannedStartTime);
|
||||||
existingDate.getUTCFullYear() === actualYear &&
|
if (
|
||||||
existingDate.getUTCMonth() === actualMonth &&
|
existingDate.getUTCFullYear() === actualYear &&
|
||||||
existingDate.getUTCDate() === actualDay
|
existingDate.getUTCMonth() === actualMonth &&
|
||||||
) {
|
existingDate.getUTCDate() === actualDay
|
||||||
const assignmentHours = differenceInHours(
|
) {
|
||||||
existing.plannedEndTime,
|
const assignmentHours = differenceInHours(
|
||||||
existing.plannedStartTime
|
existing.plannedEndTime,
|
||||||
);
|
existing.plannedStartTime
|
||||||
dailyHoursAlreadyAssigned += assignmentHours;
|
);
|
||||||
|
dailyHoursAlreadyAssigned += assignmentHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if new assignment would exceed daily limit
|
||||||
|
if (dailyHoursAlreadyAssigned + durationHours > maxDailyHours) {
|
||||||
|
const excessHours = (dailyHoursAlreadyAssigned + durationHours) - maxDailyHours;
|
||||||
|
throw new Error(
|
||||||
|
`Limite giornaliero superato: la guardia ha già ${dailyHoursAlreadyAssigned}h assegnate il ${shiftDate.toLocaleDateString('it-IT')}. ` +
|
||||||
|
`Aggiungendo ${durationHours}h si supererebbero di ${excessHours}h le ${maxDailyHours}h massime giornaliere (CCNL).`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if new assignment would exceed daily limit
|
|
||||||
if (dailyHoursAlreadyAssigned + durationHours > maxDailyHours) {
|
|
||||||
throw new Error(
|
|
||||||
`Limite giornaliero superato: la guardia ha già ${dailyHoursAlreadyAssigned}h assegnate il ${shiftDate.toLocaleDateString('it-IT')}. ` +
|
|
||||||
`Aggiungendo ${durationHours}h si supererebbero le ${maxDailyHours}h massime giornaliere (CCNL).`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create assignment for this day
|
// Create assignment for this day
|
||||||
@ -1422,8 +1431,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
if (errorMessage.includes('overlap') ||
|
if (errorMessage.includes('overlap') ||
|
||||||
errorMessage.includes('conflict') ||
|
errorMessage.includes('conflict') ||
|
||||||
errorMessage.includes('conflitto') ||
|
errorMessage.includes('conflitto') ||
|
||||||
errorMessage.includes('già assegnata')) {
|
errorMessage.includes('già assegnata') ||
|
||||||
return res.status(409).json({ message: error.message });
|
errorMessage.includes('limite giornaliero') ||
|
||||||
|
errorMessage.includes('limite settimanale') ||
|
||||||
|
errorMessage.includes('ccnl')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
message: error.message,
|
||||||
|
type: errorMessage.includes('limite') ? 'CCNL_VIOLATION' : 'CONFLICT'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
res.status(500).json({ message: "Failed to assign guard", error: String(error) });
|
res.status(500).json({ message: "Failed to assign guard", error: String(error) });
|
||||||
}
|
}
|
||||||
@ -1966,6 +1981,67 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= CUSTOMER ROUTES =============
|
||||||
|
app.get("/api/customers", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const customers = await storage.getAllCustomers();
|
||||||
|
res.json(customers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching customers:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch customers" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/customers", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const validatedData = insertCustomerSchema.parse(req.body);
|
||||||
|
const customer = await storage.createCustomer(validatedData);
|
||||||
|
res.json(customer);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "Validation failed",
|
||||||
|
errors: fromZodError(error).message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error("Error creating customer:", error);
|
||||||
|
res.status(500).json({ message: "Failed to create customer" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/api/customers/:id", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const validatedData = insertCustomerSchema.partial().parse(req.body);
|
||||||
|
const customer = await storage.updateCustomer(req.params.id, validatedData);
|
||||||
|
if (!customer) {
|
||||||
|
return res.status(404).json({ message: "Customer not found" });
|
||||||
|
}
|
||||||
|
res.json(customer);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "Validation failed",
|
||||||
|
errors: fromZodError(error).message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error("Error updating customer:", error);
|
||||||
|
res.status(500).json({ message: "Failed to update customer" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/customers/:id", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const customer = await storage.deleteCustomer(req.params.id);
|
||||||
|
if (!customer) {
|
||||||
|
return res.status(404).json({ message: "Customer not found" });
|
||||||
|
}
|
||||||
|
res.json(customer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting customer:", error);
|
||||||
|
res.status(500).json({ message: "Failed to delete customer" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============= SITE ROUTES =============
|
// ============= SITE ROUTES =============
|
||||||
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
guards,
|
guards,
|
||||||
certifications,
|
certifications,
|
||||||
vehicles,
|
vehicles,
|
||||||
|
customers,
|
||||||
sites,
|
sites,
|
||||||
shifts,
|
shifts,
|
||||||
shiftAssignments,
|
shiftAssignments,
|
||||||
@ -26,6 +27,8 @@ import {
|
|||||||
type InsertCertification,
|
type InsertCertification,
|
||||||
type Vehicle,
|
type Vehicle,
|
||||||
type InsertVehicle,
|
type InsertVehicle,
|
||||||
|
type Customer,
|
||||||
|
type InsertCustomer,
|
||||||
type Site,
|
type Site,
|
||||||
type InsertSite,
|
type InsertSite,
|
||||||
type Shift,
|
type Shift,
|
||||||
@ -85,6 +88,13 @@ export interface IStorage {
|
|||||||
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
|
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
|
||||||
deleteServiceType(id: string): Promise<ServiceType | undefined>;
|
deleteServiceType(id: string): Promise<ServiceType | undefined>;
|
||||||
|
|
||||||
|
// Customer operations
|
||||||
|
getAllCustomers(): Promise<Customer[]>;
|
||||||
|
getCustomer(id: string): Promise<Customer | undefined>;
|
||||||
|
createCustomer(customer: InsertCustomer): Promise<Customer>;
|
||||||
|
updateCustomer(id: string, customer: Partial<InsertCustomer>): Promise<Customer | undefined>;
|
||||||
|
deleteCustomer(id: string): Promise<Customer | undefined>;
|
||||||
|
|
||||||
// Site operations
|
// Site operations
|
||||||
getAllSites(): Promise<Site[]>;
|
getAllSites(): Promise<Site[]>;
|
||||||
getSite(id: string): Promise<Site | undefined>;
|
getSite(id: string): Promise<Site | undefined>;
|
||||||
@ -342,6 +352,35 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Customer operations
|
||||||
|
async getAllCustomers(): Promise<Customer[]> {
|
||||||
|
return await db.select().from(customers).orderBy(desc(customers.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomer(id: string): Promise<Customer | undefined> {
|
||||||
|
const [customer] = await db.select().from(customers).where(eq(customers.id, id));
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomer(customer: InsertCustomer): Promise<Customer> {
|
||||||
|
const [newCustomer] = await db.insert(customers).values(customer).returning();
|
||||||
|
return newCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomer(id: string, customerData: Partial<InsertCustomer>): Promise<Customer | undefined> {
|
||||||
|
const [updated] = await db
|
||||||
|
.update(customers)
|
||||||
|
.set({ ...customerData, updatedAt: new Date() })
|
||||||
|
.where(eq(customers.id, id))
|
||||||
|
.returning();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomer(id: string): Promise<Customer | undefined> {
|
||||||
|
const [deleted] = await db.delete(customers).where(eq(customers.id, id)).returning();
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
// Site operations
|
// Site operations
|
||||||
async getAllSites(): Promise<Site[]> {
|
async getAllSites(): Promise<Site[]> {
|
||||||
return await db.select().from(sites);
|
return await db.select().from(sites);
|
||||||
|
|||||||
@ -200,13 +200,35 @@ export const serviceTypes = pgTable("service_types", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= CUSTOMERS =============
|
||||||
|
|
||||||
|
export const customers = pgTable("customers", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
businessName: varchar("business_name"), // Ragione sociale
|
||||||
|
vatNumber: varchar("vat_number"), // Partita IVA
|
||||||
|
fiscalCode: varchar("fiscal_code"), // Codice fiscale
|
||||||
|
address: varchar("address"),
|
||||||
|
city: varchar("city"),
|
||||||
|
province: varchar("province"),
|
||||||
|
zipCode: varchar("zip_code"),
|
||||||
|
phone: varchar("phone"),
|
||||||
|
email: varchar("email"),
|
||||||
|
pec: varchar("pec"), // PEC (Posta Elettronica Certificata)
|
||||||
|
contactPerson: varchar("contact_person"), // Referente
|
||||||
|
notes: text("notes"),
|
||||||
|
isActive: boolean("is_active").default(true),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// ============= SITES & CONTRACTS =============
|
// ============= SITES & CONTRACTS =============
|
||||||
|
|
||||||
export const sites = pgTable("sites", {
|
export const sites = pgTable("sites", {
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
address: varchar("address").notNull(),
|
address: varchar("address").notNull(),
|
||||||
clientId: varchar("client_id").references(() => users.id),
|
customerId: varchar("customer_id").references(() => customers.id),
|
||||||
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
|
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
|
||||||
|
|
||||||
// Service requirements
|
// Service requirements
|
||||||
@ -478,7 +500,6 @@ export const usersRelations = relations(users, ({ one, many }) => ({
|
|||||||
fields: [users.id],
|
fields: [users.id],
|
||||||
references: [guards.userId],
|
references: [guards.userId],
|
||||||
}),
|
}),
|
||||||
managedSites: many(sites),
|
|
||||||
notifications: many(notifications),
|
notifications: many(notifications),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -510,10 +531,14 @@ export const certificationsRelations = relations(certifications, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const customersRelations = relations(customers, ({ many }) => ({
|
||||||
|
sites: many(sites),
|
||||||
|
}));
|
||||||
|
|
||||||
export const sitesRelations = relations(sites, ({ one, many }) => ({
|
export const sitesRelations = relations(sites, ({ one, many }) => ({
|
||||||
client: one(users, {
|
customer: one(customers, {
|
||||||
fields: [sites.clientId],
|
fields: [sites.customerId],
|
||||||
references: [users.id],
|
references: [customers.id],
|
||||||
}),
|
}),
|
||||||
shifts: many(shifts),
|
shifts: many(shifts),
|
||||||
preferences: many(sitePreferences),
|
preferences: many(sitePreferences),
|
||||||
@ -680,6 +705,12 @@ export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertCustomerSchema = createInsertSchema(customers).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const insertSiteSchema = createInsertSchema(sites).omit({
|
export const insertSiteSchema = createInsertSchema(sites).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
@ -794,6 +825,9 @@ export type Vehicle = typeof vehicles.$inferSelect;
|
|||||||
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
|
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
|
||||||
export type ServiceType = typeof serviceTypes.$inferSelect;
|
export type ServiceType = typeof serviceTypes.$inferSelect;
|
||||||
|
|
||||||
|
export type InsertCustomer = z.infer<typeof insertCustomerSchema>;
|
||||||
|
export type Customer = typeof customers.$inferSelect;
|
||||||
|
|
||||||
export type InsertSite = z.infer<typeof insertSiteSchema>;
|
export type InsertSite = z.infer<typeof insertSiteSchema>;
|
||||||
export type Site = typeof sites.$inferSelect;
|
export type Site = typeof sites.$inferSelect;
|
||||||
|
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.33",
|
"version": "1.0.34",
|
||||||
"lastUpdate": "2025-10-22T08:52:21.600Z",
|
"lastUpdate": "2025-10-23T08:03:06.051Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"version": "1.0.34",
|
||||||
|
"date": "2025-10-23",
|
||||||
|
"type": "patch",
|
||||||
|
"description": "Deployment automatico v1.0.34"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.33",
|
"version": "1.0.33",
|
||||||
"date": "2025-10-22",
|
"date": "2025-10-22",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user