From 4ed700daeef28d8a277564ef960a8ea12ab25818 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 11 Oct 2025 19:10:09 +0000 Subject: [PATCH] Add system for managing guard preferences and site assignments Introduce new schema definitions for guard constraints, site preferences, contract parameters, and training/absence tracking. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/H8Wilyj --- .replit | 4 + shared/schema.ts | 350 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) diff --git a/.replit b/.replit index 03709f0..d41ea9d 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,10 @@ externalPort = 3001 localPort = 41343 externalPort = 3000 +[[ports]] +localPort = 42175 +externalPort = 3002 + [env] PORT = "5000" diff --git a/shared/schema.ts b/shared/schema.ts index cb1f89b..5b6ccc7 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -6,6 +6,7 @@ import { text, timestamp, index, + uniqueIndex, jsonb, boolean, integer, @@ -44,6 +45,32 @@ export const certificationStatusEnum = pgEnum("certification_status", [ "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 +]); + // ============= SESSION & AUTH TABLES (Replit Auth) ============= // Session storage table - mandatory for Replit Auth @@ -167,6 +194,170 @@ export const notifications = pgTable("notifications", { 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), + minWeeklyRestHours: integer("min_weekly_rest_hours").notNull().default(24), + + // 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 }) => ({ @@ -185,6 +376,10 @@ export const guardsRelations = relations(guards, ({ one, many }) => ({ }), certifications: many(certifications), shiftAssignments: many(shiftAssignments), + constraints: one(guardConstraints), + sitePreferences: many(sitePreferences), + trainingCourses: many(trainingCourses), + absences: many(absences), })); export const certificationsRelations = relations(certifications, ({ one }) => ({ @@ -200,6 +395,7 @@ export const sitesRelations = relations(sites, ({ one, many }) => ({ references: [users.id], }), shifts: many(shifts), + preferences: many(sitePreferences), })); export const shiftsRelations = relations(shifts, ({ one, many }) => ({ @@ -228,6 +424,77 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ }), })); +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({ @@ -284,6 +551,48 @@ export const insertNotificationSchema = createInsertSchema(notifications).omit({ 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; @@ -307,6 +616,30 @@ export type ShiftAssignment = typeof shiftAssignments.$inferSelect; export type InsertNotification = z.infer; export type Notification = typeof notifications.$inferSelect; +export type InsertGuardConstraints = z.infer; +export type GuardConstraints = typeof guardConstraints.$inferSelect; + +export type InsertSitePreference = z.infer; +export type SitePreference = typeof sitePreferences.$inferSelect; + +export type InsertContractParameters = z.infer; +export type ContractParameters = typeof contractParameters.$inferSelect; + +export type InsertTrainingCourse = z.infer; +export type TrainingCourse = typeof trainingCourses.$inferSelect; + +export type InsertHoliday = z.infer; +export type Holiday = typeof holidays.$inferSelect; + +export type InsertAbsence = z.infer; +export type Absence = typeof absences.$inferSelect; + +export type InsertHolidayAssignment = z.infer; +export type HolidayAssignment = typeof holidayAssignments.$inferSelect; + +export type InsertAbsenceAffectedShift = z.infer; +export type AbsenceAffectedShift = typeof absenceAffectedShifts.$inferSelect; + // ============= EXTENDED TYPES FOR FRONTEND ============= export type GuardWithCertifications = Guard & { @@ -325,3 +658,20 @@ 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; + })[]; +};