VigilanzaTurni/shared/schema.ts
marco370 1caf5c4199 Improve guard shift assignment with planned start and end times
Update the schema for shift assignments to include planned start and end times, and extend the insert schema to validate these times, ensuring guards are assigned to specific time slots.

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/McZSLgC
2025-10-21 06:33:40 +00:00

896 lines
31 KiB
TypeScript

import { sql } from "drizzle-orm";
import { relations } from "drizzle-orm";
import {
pgTable,
varchar,
text,
timestamp,
index,
uniqueIndex,
jsonb,
boolean,
integer,
date,
pgEnum,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
// ============= ENUMS =============
export const userRoleEnum = pgEnum("user_role", [
"admin",
"coordinator",
"guard",
"client",
]);
export const shiftTypeEnum = pgEnum("shift_type", [
"fixed_post", // Presidio fisso
"patrol", // Pattugliamento/ronde
"night_inspection", // Ispettorato notturno
"quick_response", // Pronto intervento
]);
export const shiftStatusEnum = pgEnum("shift_status", [
"planned",
"active",
"completed",
"cancelled",
]);
export const certificationStatusEnum = pgEnum("certification_status", [
"valid",
"expiring_soon", // < 30 days
"expired",
]);
export const shiftPreferenceEnum = pgEnum("shift_preference", [
"morning", // 06:00-14:00
"afternoon", // 14:00-22:00
"night", // 22:00-06:00
"any",
]);
export const absenceTypeEnum = pgEnum("absence_type", [
"sick_leave", // Malattia
"vacation", // Ferie
"personal_leave", // Permesso
"injury", // Infortunio
]);
export const trainingStatusEnum = pgEnum("training_status", [
"scheduled",
"completed",
"expired",
"cancelled",
]);
export const sitePreferenceTypeEnum = pgEnum("site_preference_type", [
"preferred", // Continuità - operatore preferito per questo sito
"blacklisted", // Non assegnare mai questo operatore a questo sito
]);
export const vehicleStatusEnum = pgEnum("vehicle_status", [
"available", // Disponibile
"in_use", // In uso
"maintenance", // In manutenzione
"out_of_service", // Fuori servizio
]);
export const vehicleTypeEnum = pgEnum("vehicle_type", [
"car", // Auto
"van", // Furgone
"motorcycle", // Moto
"suv", // SUV
]);
export const locationEnum = pgEnum("location", [
"roccapiemonte", // Sede Roccapiemonte (Salerno)
"milano", // Sede Milano
"roma", // Sede Roma
]);
// ============= SESSION & AUTH TABLES (Replit Auth) =============
// Session storage table - mandatory for Replit Auth
export const sessions = pgTable(
"sessions",
{
sid: varchar("sid").primaryKey(),
sess: jsonb("sess").notNull(),
expire: timestamp("expire").notNull(),
},
(table) => [index("IDX_session_expire").on(table.expire)]
);
// User storage table - mandatory for Replit Auth
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
email: varchar("email").unique(),
firstName: varchar("first_name"),
lastName: varchar("last_name"),
profileImageUrl: varchar("profile_image_url"),
passwordHash: varchar("password_hash"), // For local auth - bcrypt hash
role: userRoleEnum("role").notNull().default("guard"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= GUARDS & CERTIFICATIONS =============
export const guards = pgTable("guards", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").references(() => users.id),
// Anagrafica
firstName: varchar("first_name").notNull(),
lastName: varchar("last_name").notNull(),
email: varchar("email"),
badgeNumber: varchar("badge_number").notNull().unique(),
phoneNumber: varchar("phone_number"),
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede di appartenenza
// Skills
isArmed: boolean("is_armed").default(false),
hasFireSafety: boolean("has_fire_safety").default(false),
hasFirstAid: boolean("has_first_aid").default(false),
hasDriverLicense: boolean("has_driver_license").default(false),
languages: text("languages").array(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
export const certifications = pgTable("certifications", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
type: varchar("type").notNull(), // porto_armi, medical_exam, training_course
name: varchar("name").notNull(),
issueDate: date("issue_date").notNull(),
expiryDate: date("expiry_date").notNull(),
status: certificationStatusEnum("status").notNull().default("valid"),
documentUrl: varchar("document_url"),
createdAt: timestamp("created_at").defaultNow(),
});
// ============= VEHICLES (PARCO AUTOMEZZI) =============
export const vehicles = pgTable("vehicles", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
licensePlate: varchar("license_plate").notNull().unique(), // Targa
brand: varchar("brand").notNull(), // Marca (es: Fiat, Volkswagen)
model: varchar("model").notNull(), // Modello
vehicleType: vehicleTypeEnum("vehicle_type").notNull(),
year: integer("year"), // Anno immatricolazione
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede di appartenenza
// Assegnazione
assignedGuardId: varchar("assigned_guard_id").references(() => guards.id, { onDelete: "set null" }),
// Stato e manutenzione
status: vehicleStatusEnum("status").notNull().default("available"),
lastMaintenanceDate: date("last_maintenance_date"),
nextMaintenanceDate: date("next_maintenance_date"),
mileage: integer("mileage"), // Chilometraggio
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SERVICE TYPES =============
export const serviceTypes = pgTable("service_types", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
code: varchar("code").notNull().unique(), // fixed_post, patrol, etc.
label: varchar("label").notNull(), // Presidio Fisso, Pattugliamento, etc.
description: text("description"), // Descrizione dettagliata
icon: varchar("icon").notNull().default("Building2"), // Nome icona Lucide
color: varchar("color").notNull().default("blue"), // blue, green, purple, orange
// Parametri specifici per tipo servizio
fixedPostHours: integer("fixed_post_hours"), // Ore presidio fisso (es. 8, 12)
patrolPassages: integer("patrol_passages"), // Numero passaggi pattugliamento (es. 3, 5)
inspectionFrequency: integer("inspection_frequency"), // Frequenza ispezioni in minuti
responseTimeMinutes: integer("response_time_minutes"), // Tempo risposta pronto intervento
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SITES & CONTRACTS =============
export const sites = pgTable("sites", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
name: varchar("name").notNull(),
address: varchar("address").notNull(),
clientId: varchar("client_id").references(() => users.id),
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
// Service requirements
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),
// Orari servizio (formato HH:MM, es. "08:00", "20:00")
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"),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SHIFTS & ASSIGNMENTS =============
export const shifts = pgTable("shifts", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
siteId: varchar("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
startTime: timestamp("start_time").notNull(),
endTime: timestamp("end_time").notNull(),
status: shiftStatusEnum("status").notNull().default("planned"),
// Veicolo assegnato al turno (opzionale)
vehicleId: varchar("vehicle_id").references(() => vehicles.id, { onDelete: "set null" }),
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
export const shiftAssignments = pgTable("shift_assignments", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
shiftId: varchar("shift_id").notNull().references(() => shifts.id, { onDelete: "cascade" }),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
// Planned shift times (when the guard is scheduled to work)
plannedStartTime: timestamp("planned_start_time").notNull(),
plannedEndTime: timestamp("planned_end_time").notNull(),
assignedAt: timestamp("assigned_at").defaultNow(),
confirmedAt: timestamp("confirmed_at"),
// Actual check-in/out times (recorded when guard clocks in/out)
checkInTime: timestamp("check_in_time"),
checkOutTime: timestamp("check_out_time"),
});
// ============= CCNL SETTINGS =============
export const ccnlSettings = pgTable("ccnl_settings", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
key: varchar("key").notNull().unique(),
value: varchar("value").notNull(),
description: text("description"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= NOTIFICATIONS =============
export const notifications = pgTable("notifications", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
title: varchar("title").notNull(),
message: text("message").notNull(),
type: varchar("type").notNull(), // shift_assigned, certification_expiring, shift_reminder
isRead: boolean("is_read").default(false),
relatedEntityId: varchar("related_entity_id"), // shift_id, certification_id, etc.
createdAt: timestamp("created_at").defaultNow(),
});
// ============= GUARD CONSTRAINTS & PREFERENCES =============
export const guardConstraints = pgTable("guard_constraints", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }).unique(),
// Preferenze turni
preferredShiftType: shiftPreferenceEnum("preferred_shift_type").default("any"),
// Limiti contrattuali personalizzati (se diversi dal CCNL base)
maxHoursPerDay: integer("max_hours_per_day").default(10), // 8 + 2 straordinario
maxHoursPerWeek: integer("max_hours_per_week").default(48), // 40 + 8 straordinario
// Giorni riposo preferiti (1=Lunedì...7=Domenica)
preferredDaysOff: integer("preferred_days_off").array(),
// Disponibilità festività
availableOnHolidays: boolean("available_on_holidays").default(true),
// Note aggiuntive
notes: text("notes"),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SITE PREFERENCES =============
export const sitePreferences = pgTable("site_preferences", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
siteId: varchar("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
// Preferenza: preferred = continuità, blacklisted = non assegnare mai
preference: sitePreferenceTypeEnum("preference").notNull(),
priority: integer("priority").default(0), // Più alto = più preferito
reason: text("reason"), // Motivazione preferenza/blacklist
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
// Unique constraint: una sola preferenza per coppia sito-guardia
uniqueSiteGuard: uniqueIndex("unique_site_guard_preference").on(table.siteId, table.guardId),
}));
// ============= CONTRACT PARAMETERS (CCNL) =============
export const contractParameters = pgTable("contract_parameters", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
// Identificatore tipo contratto (default = CCNL Vigilanza Privata)
contractType: varchar("contract_type").notNull().unique().default("CCNL_VIGILANZA_2023"),
// Limiti orari
maxHoursPerDay: integer("max_hours_per_day").notNull().default(8),
maxOvertimePerDay: integer("max_overtime_per_day").notNull().default(2),
maxHoursPerWeek: integer("max_hours_per_week").notNull().default(40),
maxOvertimePerWeek: integer("max_overtime_per_week").notNull().default(8),
// Riposi obbligatori
minDailyRestHours: integer("min_daily_rest_hours").notNull().default(11),
minDailyRestHoursReduced: integer("min_daily_rest_hours_reduced").notNull().default(9), // Deroga CCNL
maxDailyRestReductionsPerMonth: integer("max_daily_rest_reductions_per_month").notNull().default(3),
maxDailyRestReductionsPerYear: integer("max_daily_rest_reductions_per_year").notNull().default(12),
minWeeklyRestHours: integer("min_weekly_rest_hours").notNull().default(24),
// Pause obbligatorie
pauseMinutesIfOver6Hours: integer("pause_minutes_if_over_6_hours").notNull().default(10),
// Buoni pasto
mealVoucherEnabled: boolean("meal_voucher_enabled").default(true), // Buoni pasto attivi
mealVoucherAfterHours: integer("meal_voucher_after_hours").default(6), // Ore minime per diritto buono pasto
mealVoucherAmount: integer("meal_voucher_amount").default(8), // Importo buono pasto in euro (facoltativo)
// Limiti notturni (22:00-06:00)
maxNightHoursPerWeek: integer("max_night_hours_per_week").default(48),
// Maggiorazioni (percentuali)
holidayPayIncrease: integer("holiday_pay_increase").default(30), // +30% festivi
nightPayIncrease: integer("night_pay_increase").default(20), // +20% notturni
overtimePayIncrease: integer("overtime_pay_increase").default(15), // +15% straordinari
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= TRAINING & COURSES =============
export const trainingCourses = pgTable("training_courses", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
courseName: varchar("course_name").notNull(),
courseType: varchar("course_type").notNull(), // "mandatory" | "optional"
scheduledDate: date("scheduled_date"),
completionDate: date("completion_date"),
expiryDate: date("expiry_date"), // Per corsi con scadenza (es. primo soccorso)
status: trainingStatusEnum("status").notNull().default("scheduled"),
certificateUrl: varchar("certificate_url"),
provider: varchar("provider"), // Ente formatore
hours: integer("hours"), // Ore corso
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow(),
});
// ============= HOLIDAYS =============
export const holidays = pgTable("holidays", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
name: varchar("name").notNull(), // "Natale", "Capodanno", "1 Maggio", etc.
date: date("date").notNull(),
year: integer("year").notNull(), // Anno specifico per tracking rotazioni
isNational: boolean("is_national").default(true), // Nazionale o regionale
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
// Unique constraint: una festività per data e anno
uniqueDateYear: uniqueIndex("unique_holiday_date_year").on(table.date, table.year),
}));
// Join table per tracking rotazioni festività con integrità referenziale
export const holidayAssignments = pgTable("holiday_assignments", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
holidayId: varchar("holiday_id").notNull().references(() => holidays.id, { onDelete: "cascade" }),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
shiftId: varchar("shift_id").references(() => shifts.id, { onDelete: "set null" }), // Turno specifico lavorato
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
// Unique constraint: una guardia non può essere assegnata due volte alla stessa festività
uniqueHolidayGuard: uniqueIndex("unique_holiday_guard").on(table.holidayId, table.guardId),
}));
// ============= ABSENCES & SUBSTITUTIONS =============
export const absences = pgTable("absences", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
guardId: varchar("guard_id").notNull().references(() => guards.id, { onDelete: "cascade" }),
type: absenceTypeEnum("type").notNull(),
startDate: date("start_date").notNull(),
endDate: date("end_date").notNull(),
// Documentazione
certificateUrl: varchar("certificate_url"), // Certificato medico, etc.
notes: text("notes"),
// Se approvata e serve sostituto
isApproved: boolean("is_approved").default(false),
needsSubstitute: boolean("needs_substitute").default(true),
substituteGuardId: varchar("substitute_guard_id").references(() => guards.id),
createdAt: timestamp("created_at").defaultNow(),
approvedAt: timestamp("approved_at"),
approvedBy: varchar("approved_by").references(() => users.id),
});
// Join table per tracking turni impattati da assenze (per sistema sostituzione)
export const absenceAffectedShifts = pgTable("absence_affected_shifts", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
absenceId: varchar("absence_id").notNull().references(() => absences.id, { onDelete: "cascade" }),
shiftId: varchar("shift_id").notNull().references(() => shifts.id, { onDelete: "cascade" }),
// Se sostituto trovato
isSubstituted: boolean("is_substituted").default(false),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
// Unique: uno shift non può essere impattato due volte dalla stessa assenza
uniqueAbsenceShift: uniqueIndex("unique_absence_shift").on(table.absenceId, table.shiftId),
}));
// ============= RELATIONS =============
export const usersRelations = relations(users, ({ one, many }) => ({
guard: one(guards, {
fields: [users.id],
references: [guards.userId],
}),
managedSites: many(sites),
notifications: many(notifications),
}));
export const guardsRelations = relations(guards, ({ one, many }) => ({
user: one(users, {
fields: [guards.userId],
references: [users.id],
}),
certifications: many(certifications),
shiftAssignments: many(shiftAssignments),
constraints: one(guardConstraints),
sitePreferences: many(sitePreferences),
trainingCourses: many(trainingCourses),
absences: many(absences),
assignedVehicles: many(vehicles),
}));
export const vehiclesRelations = relations(vehicles, ({ one }) => ({
assignedGuard: one(guards, {
fields: [vehicles.assignedGuardId],
references: [guards.id],
}),
}));
export const certificationsRelations = relations(certifications, ({ one }) => ({
guard: one(guards, {
fields: [certifications.guardId],
references: [guards.id],
}),
}));
export const sitesRelations = relations(sites, ({ one, many }) => ({
client: one(users, {
fields: [sites.clientId],
references: [users.id],
}),
shifts: many(shifts),
preferences: many(sitePreferences),
}));
export const shiftsRelations = relations(shifts, ({ one, many }) => ({
site: one(sites, {
fields: [shifts.siteId],
references: [sites.id],
}),
vehicle: one(vehicles, {
fields: [shifts.vehicleId],
references: [vehicles.id],
}),
assignments: many(shiftAssignments),
}));
export const shiftAssignmentsRelations = relations(shiftAssignments, ({ one }) => ({
shift: one(shifts, {
fields: [shiftAssignments.shiftId],
references: [shifts.id],
}),
guard: one(guards, {
fields: [shiftAssignments.guardId],
references: [guards.id],
}),
}));
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, {
fields: [notifications.userId],
references: [users.id],
}),
}));
export const guardConstraintsRelations = relations(guardConstraints, ({ one }) => ({
guard: one(guards, {
fields: [guardConstraints.guardId],
references: [guards.id],
}),
}));
export const sitePreferencesRelations = relations(sitePreferences, ({ one }) => ({
site: one(sites, {
fields: [sitePreferences.siteId],
references: [sites.id],
}),
guard: one(guards, {
fields: [sitePreferences.guardId],
references: [guards.id],
}),
}));
export const trainingCoursesRelations = relations(trainingCourses, ({ one }) => ({
guard: one(guards, {
fields: [trainingCourses.guardId],
references: [guards.id],
}),
}));
export const absencesRelations = relations(absences, ({ one, many }) => ({
guard: one(guards, {
fields: [absences.guardId],
references: [guards.id],
}),
substituteGuard: one(guards, {
fields: [absences.substituteGuardId],
references: [guards.id],
}),
approver: one(users, {
fields: [absences.approvedBy],
references: [users.id],
}),
affectedShifts: many(absenceAffectedShifts),
}));
export const absenceAffectedShiftsRelations = relations(absenceAffectedShifts, ({ one }) => ({
absence: one(absences, {
fields: [absenceAffectedShifts.absenceId],
references: [absences.id],
}),
shift: one(shifts, {
fields: [absenceAffectedShifts.shiftId],
references: [shifts.id],
}),
}));
export const holidaysRelations = relations(holidays, ({ many }) => ({
assignments: many(holidayAssignments),
}));
export const holidayAssignmentsRelations = relations(holidayAssignments, ({ one }) => ({
holiday: one(holidays, {
fields: [holidayAssignments.holidayId],
references: [holidays.id],
}),
guard: one(guards, {
fields: [holidayAssignments.guardId],
references: [guards.id],
}),
shift: one(shifts, {
fields: [holidayAssignments.shiftId],
references: [shifts.id],
}),
}));
// ============= INSERT SCHEMAS =============
export const insertUserSchema = createInsertSchema(users).pick({
email: true,
firstName: true,
lastName: true,
profileImageUrl: true,
passwordHash: true,
role: true,
});
// Form schema with plain password (will be hashed on backend)
export const insertUserFormSchema = z.object({
email: z.string().email("Email non valida"),
firstName: z.string().min(1, "Nome obbligatorio"),
lastName: z.string().min(1, "Cognome obbligatorio"),
password: z.string().min(6, "Password minimo 6 caratteri"),
role: z.enum(["admin", "coordinator", "guard", "client"]).default("guard"),
});
// Update user form schema (password optional)
export const updateUserFormSchema = z.object({
email: z.string().email("Email non valida").optional(),
firstName: z.string().min(1, "Nome obbligatorio").optional(),
lastName: z.string().min(1, "Cognome obbligatorio").optional(),
password: z.string().min(6, "Password minimo 6 caratteri").optional(),
role: z.enum(["admin", "coordinator", "guard", "client"]).optional(),
});
export const insertGuardSchema = createInsertSchema(guards).omit({
id: true,
createdAt: true,
updatedAt: true,
}).extend({
firstName: z.string().min(1, "Nome obbligatorio"),
lastName: z.string().min(1, "Cognome obbligatorio"),
email: z.string().email("Email non valida").optional().or(z.literal("")),
badgeNumber: z.string().min(1, "Matricola obbligatoria"),
phoneNumber: z.string().optional().or(z.literal("")),
location: z.enum(["roccapiemonte", "milano", "roma"]),
});
export const insertCertificationSchema = createInsertSchema(certifications).omit({
id: true,
createdAt: true,
status: true,
});
export const insertVehicleSchema = createInsertSchema(vehicles).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertSiteSchema = createInsertSchema(sites).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertShiftSchema = createInsertSchema(shifts).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// Form schema that accepts datetime strings and transforms to Date
export const insertShiftFormSchema = z.object({
siteId: z.string().min(1, "Sito obbligatorio"),
startTime: z.string().min(1, "Data inizio obbligatoria").refine((val) => !isNaN(new Date(val).getTime()), {
message: "Data inizio non valida",
}).transform((val) => new Date(val)),
endTime: z.string().min(1, "Data fine obbligatoria").refine((val) => !isNaN(new Date(val).getTime()), {
message: "Data fine non valida",
}).transform((val) => new Date(val)),
status: z.enum(["planned", "active", "completed", "cancelled"]).default("planned"),
});
export const insertShiftAssignmentSchema = createInsertSchema(shiftAssignments)
.omit({
id: true,
assignedAt: true,
})
.extend({
plannedStartTime: z.union([z.string(), z.date()], {
required_error: "Orario inizio richiesto",
invalid_type_error: "Orario inizio non valido",
}).transform((val) => new Date(val)),
plannedEndTime: z.union([z.string(), z.date()], {
required_error: "Orario fine richiesto",
invalid_type_error: "Orario fine non valido",
}).transform((val) => new Date(val)),
})
.refine((data) => data.plannedEndTime > data.plannedStartTime, {
message: "L'orario di fine deve essere successivo all'orario di inizio",
path: ["plannedEndTime"],
});
export const insertCcnlSettingSchema = createInsertSchema(ccnlSettings).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertNotificationSchema = createInsertSchema(notifications).omit({
id: true,
createdAt: true,
});
export const insertGuardConstraintsSchema = createInsertSchema(guardConstraints).omit({
id: true,
updatedAt: true,
});
export const insertSitePreferenceSchema = createInsertSchema(sitePreferences).omit({
id: true,
createdAt: true,
});
export const insertContractParametersSchema = createInsertSchema(contractParameters).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertTrainingCourseSchema = createInsertSchema(trainingCourses).omit({
id: true,
createdAt: true,
});
export const insertHolidaySchema = createInsertSchema(holidays).omit({
id: true,
createdAt: true,
});
export const insertAbsenceSchema = createInsertSchema(absences).omit({
id: true,
createdAt: true,
approvedAt: true,
});
export const insertHolidayAssignmentSchema = createInsertSchema(holidayAssignments).omit({
id: true,
createdAt: true,
});
export const insertAbsenceAffectedShiftSchema = createInsertSchema(absenceAffectedShifts).omit({
id: true,
createdAt: true,
});
// ============= TYPES =============
export type UpsertUser = typeof users.$inferInsert;
export type User = typeof users.$inferSelect;
export type InsertGuard = z.infer<typeof insertGuardSchema>;
export type Guard = typeof guards.$inferSelect;
export type InsertCertification = z.infer<typeof insertCertificationSchema>;
export type Certification = typeof certifications.$inferSelect;
export type InsertVehicle = z.infer<typeof insertVehicleSchema>;
export type Vehicle = typeof vehicles.$inferSelect;
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
export type ServiceType = typeof serviceTypes.$inferSelect;
export type InsertSite = z.infer<typeof insertSiteSchema>;
export type Site = typeof sites.$inferSelect;
export type InsertShift = z.infer<typeof insertShiftSchema>;
export type Shift = typeof shifts.$inferSelect;
export type InsertShiftAssignment = z.infer<typeof insertShiftAssignmentSchema>;
export type ShiftAssignment = typeof shiftAssignments.$inferSelect;
export type InsertCcnlSetting = z.infer<typeof insertCcnlSettingSchema>;
export type CcnlSetting = typeof ccnlSettings.$inferSelect;
export type InsertNotification = z.infer<typeof insertNotificationSchema>;
export type Notification = typeof notifications.$inferSelect;
export type InsertGuardConstraints = z.infer<typeof insertGuardConstraintsSchema>;
export type GuardConstraints = typeof guardConstraints.$inferSelect;
export type InsertSitePreference = z.infer<typeof insertSitePreferenceSchema>;
export type SitePreference = typeof sitePreferences.$inferSelect;
export type InsertContractParameters = z.infer<typeof insertContractParametersSchema>;
export type ContractParameters = typeof contractParameters.$inferSelect;
export type InsertTrainingCourse = z.infer<typeof insertTrainingCourseSchema>;
export type TrainingCourse = typeof trainingCourses.$inferSelect;
export type InsertHoliday = z.infer<typeof insertHolidaySchema>;
export type Holiday = typeof holidays.$inferSelect;
export type InsertAbsence = z.infer<typeof insertAbsenceSchema>;
export type Absence = typeof absences.$inferSelect;
export type InsertHolidayAssignment = z.infer<typeof insertHolidayAssignmentSchema>;
export type HolidayAssignment = typeof holidayAssignments.$inferSelect;
export type InsertAbsenceAffectedShift = z.infer<typeof insertAbsenceAffectedShiftSchema>;
export type AbsenceAffectedShift = typeof absenceAffectedShifts.$inferSelect;
// ============= EXTENDED TYPES FOR FRONTEND =============
export type GuardWithCertifications = Guard & {
certifications: Certification[];
user?: User;
};
export type ShiftWithDetails = Shift & {
site: Site;
assignments: (ShiftAssignment & {
guard: GuardWithCertifications;
})[];
};
export type SiteWithShifts = Site & {
shifts: Shift[];
client?: User;
};
export type GuardWithDetails = Guard & {
user?: User;
certifications: Certification[];
constraints?: GuardConstraints;
trainingCourses: TrainingCourse[];
absences: Absence[];
};
export type AbsenceWithDetails = Absence & {
guard: Guard;
substituteGuard?: Guard;
approver?: User;
affectedShifts: (AbsenceAffectedShift & {
shift: Shift;
})[];
};
// ============= DTOs FOR GENERAL PLANNING =============
// DTO per disponibilità guardia nella settimana
export const guardAvailabilitySchema = z.object({
guardId: z.string(),
guardName: z.string(),
badgeNumber: z.string(),
weeklyHoursRemaining: z.number(),
weeklyHoursAssigned: z.number(),
weeklyHoursMax: z.number(),
});
export type GuardAvailability = z.infer<typeof guardAvailabilitySchema>;
// DTO per creazione turno multi-giorno dal Planning Generale
export const createMultiDayShiftSchema = z.object({
siteId: z.string(),
startDate: z.string(), // YYYY-MM-DD
days: z.number().min(1).max(7),
guardId: z.string(),
shiftType: z.enum(["fixed_post", "patrol", "night_inspection", "quick_response"]).optional(),
});
export type CreateMultiDayShiftRequest = z.infer<typeof createMultiDayShiftSchema>;