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
This commit is contained in:
marco370 2025-10-11 19:10:09 +00:00
parent bd70fb9df6
commit 4ed700daee
2 changed files with 354 additions and 0 deletions

View File

@ -22,6 +22,10 @@ externalPort = 3001
localPort = 41343
externalPort = 3000
[[ports]]
localPort = 42175
externalPort = 3002
[env]
PORT = "5000"

View File

@ -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<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 & {
@ -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;
})[];
};