Compare commits

..

4 Commits

Author SHA1 Message Date
Marco Lanzara
9d33dbfa22 🚀 Release v1.0.16
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.16_20251017_151927.sql.gz
- Data: 2025-10-17 15:19:44
2025-10-17 15:19:44 +00:00
marco370
470cd9262b Add contract management and service type linkage to site data
Update the database schema to include contract details for sites and link sites to service types, while making shift type optional.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/FlO7tHX
2025-10-17 14:57:46 +00:00
marco370
e4d3ab514c Add contract start and end dates for sites and validate shifts
Implement contract start/end date validation for sites and enforce shift creation within contract boundaries on the server. Add contract status display to the client.

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/FlO7tHX
2025-10-17 14:35:51 +00:00
marco370
c8b273d9a6 Add contract details and service times to site management
Introduce new fields for contract reference, start/end dates, and service times in the `sites` schema and UI for managing site contracts.

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/UBH5igx
2025-10-17 14:29:48 +00:00
7 changed files with 316 additions and 6 deletions

View File

@ -43,6 +43,11 @@ export default function Sites() {
minGuards: 1, minGuards: 1,
requiresArmed: false, requiresArmed: false,
requiresDriverLicense: false, requiresDriverLicense: false,
contractReference: "",
contractStartDate: undefined,
contractEndDate: undefined,
serviceStartTime: "",
serviceEndTime: "",
isActive: true, isActive: true,
}, },
}); });
@ -56,6 +61,11 @@ export default function Sites() {
minGuards: 1, minGuards: 1,
requiresArmed: false, requiresArmed: false,
requiresDriverLicense: false, requiresDriverLicense: false,
contractReference: "",
contractStartDate: undefined,
contractEndDate: undefined,
serviceStartTime: "",
serviceEndTime: "",
isActive: true, isActive: true,
}, },
}); });
@ -123,10 +133,41 @@ export default function Sites() {
minGuards: site.minGuards, minGuards: site.minGuards,
requiresArmed: site.requiresArmed, requiresArmed: site.requiresArmed,
requiresDriverLicense: site.requiresDriverLicense, requiresDriverLicense: site.requiresDriverLicense,
contractReference: site.contractReference || "",
contractStartDate: site.contractStartDate || undefined,
contractEndDate: site.contractEndDate || undefined,
serviceStartTime: site.serviceStartTime || "",
serviceEndTime: site.serviceEndTime || "",
isActive: site.isActive, isActive: site.isActive,
}); });
}; };
// Funzione per determinare lo stato del contratto
const getContractStatus = (site: Site): "active" | "expiring" | "expired" | "none" => {
if (!site.contractStartDate || !site.contractEndDate) return "none";
const today = new Date();
const startDate = new Date(site.contractStartDate);
const endDate = new Date(site.contractEndDate);
if (today < startDate) return "none"; // Contratto non ancora iniziato
if (today > endDate) return "expired";
// Calcola i giorni rimanenti
const daysLeft = Math.ceil((endDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (daysLeft <= 30) return "expiring"; // In scadenza se mancano 30 giorni o meno
return "active";
};
const contractStatusLabels = {
active: { label: "Contratto Attivo", variant: "default" as const },
expiring: { label: "In Scadenza", variant: "outline" as const },
expired: { label: "Scaduto", variant: "destructive" as const },
none: { label: "Nessun Contratto", variant: "secondary" as const },
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -180,6 +221,64 @@ export default function Sites() {
)} )}
/> />
<div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Dati Contrattuali</p>
<FormField
control={form.control}
name="contractReference"
render={({ field }) => (
<FormItem>
<FormLabel>Riferimento Contratto</FormLabel>
<FormControl>
<Input placeholder="CT-2025-001" {...field} value={field.value || ""} data-testid="input-contract-reference" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="contractStartDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data Inizio Contratto</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value || ""}
data-testid="input-contract-start-date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="contractEndDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data Fine Contratto</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value || ""}
data-testid="input-contract-end-date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="shiftType" name="shiftType"
@ -253,6 +352,49 @@ export default function Sites() {
/> />
</div> </div>
<div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Orari Servizio</p>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="serviceStartTime"
render={({ field }) => (
<FormItem>
<FormLabel>Orario Inizio</FormLabel>
<FormControl>
<Input
type="time"
{...field}
value={field.value || ""}
data-testid="input-service-start-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="serviceEndTime"
render={({ field }) => (
<FormItem>
<FormLabel>Orario Fine</FormLabel>
<FormControl>
<Input
type="time"
{...field}
value={field.value || ""}
data-testid="input-service-end-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button <Button
type="button" type="button"
@ -317,6 +459,64 @@ export default function Sites() {
)} )}
/> />
<div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Dati Contrattuali</p>
<FormField
control={editForm.control}
name="contractReference"
render={({ field }) => (
<FormItem>
<FormLabel>Riferimento Contratto</FormLabel>
<FormControl>
<Input placeholder="CT-2025-001" {...field} value={field.value || ""} data-testid="input-edit-contract-reference" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="contractStartDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data Inizio Contratto</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value || ""}
data-testid="input-edit-contract-start-date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="contractEndDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data Fine Contratto</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value || ""}
data-testid="input-edit-contract-end-date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<FormField <FormField
control={editForm.control} control={editForm.control}
name="shiftType" name="shiftType"
@ -403,6 +603,49 @@ export default function Sites() {
/> />
</div> </div>
<div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Orari Servizio</p>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="serviceStartTime"
render={({ field }) => (
<FormItem>
<FormLabel>Orario Inizio</FormLabel>
<FormControl>
<Input
type="time"
{...field}
value={field.value || ""}
data-testid="input-edit-service-start-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="serviceEndTime"
render={({ field }) => (
<FormItem>
<FormLabel>Orario Fine</FormLabel>
<FormControl>
<Input
type="time"
{...field}
value={field.value || ""}
data-testid="input-edit-service-end-time"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button <Button
type="button" type="button"
@ -462,12 +705,28 @@ export default function Sites() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div> <div className="flex flex-wrap gap-2">
<Badge variant="outline"> <Badge variant="outline">
{shiftTypeLabels[site.shiftType]} {shiftTypeLabels[site.shiftType]}
</Badge> </Badge>
{(() => {
const status = getContractStatus(site);
const statusInfo = contractStatusLabels[status];
return (
<Badge variant={statusInfo.variant} data-testid={`badge-contract-status-${site.id}`}>
{statusInfo.label}
</Badge>
);
})()}
</div> </div>
{site.contractReference && (
<div className="text-xs text-muted-foreground">
Contratto: {site.contractReference}
{site.contractEndDate && ` • Scade: ${new Date(site.contractEndDate).toLocaleDateString('it-IT')}`}
</div>
)}
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" /> <Users className="h-4 w-4 text-muted-foreground" />

View File

@ -33,6 +33,8 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
**Recent Schema Updates (October 2025)**: **Recent Schema Updates (October 2025)**:
- Service types now include specialized parameters: `fixedPostHours` (ore presidio fisso), `patrolPassages` (numero passaggi pattuglia), `inspectionFrequency` (frequenza ispezioni), `responseTimeMinutes` (tempo risposta pronto intervento) - Service types now include specialized parameters: `fixedPostHours` (ore presidio fisso), `patrolPassages` (numero passaggi pattuglia), `inspectionFrequency` (frequenza ispezioni), `responseTimeMinutes` (tempo risposta pronto intervento)
- 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)
- Sites now reference service types via `serviceTypeId` foreign key; `shiftType` is optional and can be derived from service type
### 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.
@ -49,7 +51,7 @@ 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). - **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.
- **Pianificazione Operativa Interattiva**: Three-step workflow for shift assignment: - **Pianificazione Operativa Interattiva**: Three-step workflow for shift assignment:
1. Select date → view uncovered sites with coverage status 1. Select date → view uncovered sites with coverage status
2. Select site → view filtered resources (guards and vehicles matching requirements) 2. Select site → view filtered resources (guards and vehicles matching requirements)

View File

@ -944,7 +944,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(400).json({ message: "Missing required fields" }); return res.status(400).json({ message: "Missing required fields" });
} }
// Convert and validate dates // Verifica stato contratto del sito
const site = await storage.getSite(req.body.siteId);
if (!site) {
return res.status(404).json({ message: "Sito non trovato" });
}
// Controllo validità contratto - richiesto per creare turni
if (!site.contractStartDate || !site.contractEndDate) {
return res.status(400).json({
message: `Impossibile creare turno: il sito "${site.name}" non ha un contratto attivo`
});
}
// Convert and validate shift dates first
const startTime = new Date(req.body.startTime); const startTime = new Date(req.body.startTime);
const endTime = new Date(req.body.endTime); const endTime = new Date(req.body.endTime);
@ -952,6 +965,30 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(400).json({ message: "Invalid date format" }); return res.status(400).json({ message: "Invalid date format" });
} }
// Normalizza date contratto a giorno intero (00:00 - 23:59)
const contractStart = new Date(site.contractStartDate);
contractStart.setHours(0, 0, 0, 0);
const contractEnd = new Date(site.contractEndDate);
contractEnd.setHours(23, 59, 59, 999);
// Normalizza data turno a giorno (per confronto)
const shiftDate = new Date(startTime);
shiftDate.setHours(0, 0, 0, 0);
// Verifica che il turno sia dentro il periodo contrattuale
if (shiftDate > contractEnd) {
return res.status(400).json({
message: `Impossibile creare turno: il contratto per il sito "${site.name}" scade il ${new Date(site.contractEndDate).toLocaleDateString('it-IT')}`
});
}
if (shiftDate < contractStart) {
return res.status(400).json({
message: `Impossibile creare turno: il contratto per il sito "${site.name}" inizia il ${new Date(site.contractStartDate).toLocaleDateString('it-IT')}`
});
}
// Validate and transform the request body // Validate and transform the request body
const validatedData = insertShiftSchema.parse({ const validatedData = insertShiftSchema.parse({
siteId: req.body.siteId, siteId: req.body.siteId,

View File

@ -205,7 +205,8 @@ export const sites = pgTable("sites", {
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
// Service requirements // Service requirements
shiftType: shiftTypeEnum("shift_type").notNull(), serviceTypeId: varchar("service_type_id").references(() => serviceTypes.id),
shiftType: shiftTypeEnum("shift_type"), // Optional - can be derived from service type
minGuards: integer("min_guards").notNull().default(1), minGuards: integer("min_guards").notNull().default(1),
requiresArmed: boolean("requires_armed").default(false), requiresArmed: boolean("requires_armed").default(false),
requiresDriverLicense: boolean("requires_driver_license").default(false), requiresDriverLicense: boolean("requires_driver_license").default(false),
@ -214,6 +215,11 @@ export const sites = pgTable("sites", {
serviceStartTime: varchar("service_start_time"), // Orario inizio servizio serviceStartTime: varchar("service_start_time"), // Orario inizio servizio
serviceEndTime: varchar("service_end_time"), // Orario fine servizio serviceEndTime: varchar("service_end_time"), // Orario fine servizio
// Dati contrattuali
contractReference: varchar("contract_reference"), // Riferimento/numero contratto
contractStartDate: date("contract_start_date"), // Data inizio contratto
contractEndDate: date("contract_end_date"), // Data fine contratto
// Coordinates for geofencing (future use) // Coordinates for geofencing (future use)
latitude: varchar("latitude"), latitude: varchar("latitude"),
longitude: varchar("longitude"), longitude: varchar("longitude"),

View File

@ -1,7 +1,13 @@
{ {
"version": "1.0.15", "version": "1.0.16",
"lastUpdate": "2025-10-17T14:09:32.389Z", "lastUpdate": "2025-10-17T15:19:44.130Z",
"changelog": [ "changelog": [
{
"version": "1.0.16",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.16"
},
{ {
"version": "1.0.15", "version": "1.0.15",
"date": "2025-10-17", "date": "2025-10-17",