Compare commits

..

No commits in common. "9d33dbfa228f3c346a80af584adddfcbabbb06ae" and "76af862a6b9d88e434ccb5cf630db2cb578c10f4" have entirely different histories.

7 changed files with 6 additions and 316 deletions

View File

@ -43,11 +43,6 @@ 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,
}, },
}); });
@ -61,11 +56,6 @@ 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,
}, },
}); });
@ -133,41 +123,10 @@ 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">
@ -221,64 +180,6 @@ 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"
@ -352,49 +253,6 @@ 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"
@ -459,64 +317,6 @@ 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"
@ -603,49 +403,6 @@ 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"
@ -705,28 +462,12 @@ export default function Sites() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="flex flex-wrap gap-2"> <div>
<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,8 +33,6 @@ 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.
@ -51,7 +49,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) 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).
- **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,20 +944,7 @@ 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" });
} }
// Verifica stato contratto del sito // Convert and validate dates
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);
@ -965,30 +952,6 @@ 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,8 +205,7 @@ 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
serviceTypeId: varchar("service_type_id").references(() => serviceTypes.id), shiftType: shiftTypeEnum("shift_type").notNull(),
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),
@ -215,11 +214,6 @@ 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,13 +1,7 @@
{ {
"version": "1.0.16", "version": "1.0.15",
"lastUpdate": "2025-10-17T15:19:44.130Z", "lastUpdate": "2025-10-17T14:09:32.389Z",
"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",