Compare commits
4 Commits
76af862a6b
...
9d33dbfa22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d33dbfa22 | ||
|
|
470cd9262b | ||
|
|
e4d3ab514c | ||
|
|
c8b273d9a6 |
@ -43,6 +43,11 @@ export default function Sites() {
|
||||
minGuards: 1,
|
||||
requiresArmed: false,
|
||||
requiresDriverLicense: false,
|
||||
contractReference: "",
|
||||
contractStartDate: undefined,
|
||||
contractEndDate: undefined,
|
||||
serviceStartTime: "",
|
||||
serviceEndTime: "",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
@ -56,6 +61,11 @@ export default function Sites() {
|
||||
minGuards: 1,
|
||||
requiresArmed: false,
|
||||
requiresDriverLicense: false,
|
||||
contractReference: "",
|
||||
contractStartDate: undefined,
|
||||
contractEndDate: undefined,
|
||||
serviceStartTime: "",
|
||||
serviceEndTime: "",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
@ -123,10 +133,41 @@ export default function Sites() {
|
||||
minGuards: site.minGuards,
|
||||
requiresArmed: site.requiresArmed,
|
||||
requiresDriverLicense: site.requiresDriverLicense,
|
||||
contractReference: site.contractReference || "",
|
||||
contractStartDate: site.contractStartDate || undefined,
|
||||
contractEndDate: site.contractEndDate || undefined,
|
||||
serviceStartTime: site.serviceStartTime || "",
|
||||
serviceEndTime: site.serviceEndTime || "",
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<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
|
||||
control={form.control}
|
||||
name="shiftType"
|
||||
@ -253,6 +352,49 @@ export default function Sites() {
|
||||
/>
|
||||
</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">
|
||||
<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
|
||||
control={editForm.control}
|
||||
name="shiftType"
|
||||
@ -403,6 +603,49 @@ export default function Sites() {
|
||||
/>
|
||||
</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">
|
||||
<Button
|
||||
type="button"
|
||||
@ -462,12 +705,28 @@ export default function Sites() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
{shiftTypeLabels[site.shiftType]}
|
||||
</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>
|
||||
|
||||
{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="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
|
||||
BIN
database-backups/vigilanzaturni_v1.0.16_20251017_151927.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.16_20251017_151927.sql.gz
Normal file
Binary file not shown.
Binary file not shown.
@ -33,6 +33,8 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
|
||||
**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)
|
||||
- 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
|
||||
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
|
||||
- **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 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:
|
||||
1. Select date → view uncovered sites with coverage status
|
||||
2. Select site → view filtered resources (guards and vehicles matching requirements)
|
||||
|
||||
@ -944,7 +944,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
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 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" });
|
||||
}
|
||||
|
||||
// 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
|
||||
const validatedData = insertShiftSchema.parse({
|
||||
siteId: req.body.siteId,
|
||||
|
||||
@ -205,7 +205,8 @@ export const sites = pgTable("sites", {
|
||||
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
|
||||
|
||||
// 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),
|
||||
requiresArmed: boolean("requires_armed").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
|
||||
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)
|
||||
latitude: varchar("latitude"),
|
||||
longitude: varchar("longitude"),
|
||||
|
||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
||||
{
|
||||
"version": "1.0.15",
|
||||
"lastUpdate": "2025-10-17T14:09:32.389Z",
|
||||
"version": "1.0.16",
|
||||
"lastUpdate": "2025-10-17T15:19:44.130Z",
|
||||
"changelog": [
|
||||
{
|
||||
"version": "1.0.16",
|
||||
"date": "2025-10-17",
|
||||
"type": "patch",
|
||||
"description": "Deployment automatico v1.0.16"
|
||||
},
|
||||
{
|
||||
"version": "1.0.15",
|
||||
"date": "2025-10-17",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user