Update storage interface and implementation to handle shift assignment deletion, modify getGuardsAvailability to accept specific planned start and end times, and introduce conflict detection logic for guard availability. Add new DTOs (guardConflictSchema) and update guardAvailabilitySchema to include availability status, conflicts, and unavailability reasons, enhancing shift planning accuracy. 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/ZZOTK7r
908 lines
31 KiB
TypeScript
908 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 conflitto orario guardia
|
|
export const guardConflictSchema = z.object({
|
|
from: z.date(),
|
|
to: z.date(),
|
|
siteName: z.string(),
|
|
shiftId: z.string(),
|
|
});
|
|
|
|
// DTO per disponibilità guardia con controllo conflitti orari
|
|
export const guardAvailabilitySchema = z.object({
|
|
guardId: z.string(),
|
|
guardName: z.string(),
|
|
badgeNumber: z.string(),
|
|
weeklyHoursRemaining: z.number(),
|
|
weeklyHoursAssigned: z.number(),
|
|
weeklyHoursMax: z.number(),
|
|
isAvailable: z.boolean(),
|
|
conflicts: z.array(guardConflictSchema),
|
|
unavailabilityReasons: z.array(z.string()),
|
|
});
|
|
|
|
export type GuardConflict = z.infer<typeof guardConflictSchema>;
|
|
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>;
|