Implement CRUD operations for customers, including API endpoints and database schema. Rename the "Planning Generale" view to "Planning Fissi" and update related UI elements and documentation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/2w7P7NW
935 lines
31 KiB
TypeScript
935 lines
31 KiB
TypeScript
// Integration: javascript_database and javascript_log_in_with_replit blueprints
|
|
import {
|
|
users,
|
|
guards,
|
|
certifications,
|
|
vehicles,
|
|
customers,
|
|
sites,
|
|
shifts,
|
|
shiftAssignments,
|
|
notifications,
|
|
guardConstraints,
|
|
sitePreferences,
|
|
trainingCourses,
|
|
holidays,
|
|
holidayAssignments,
|
|
absences,
|
|
absenceAffectedShifts,
|
|
contractParameters,
|
|
serviceTypes,
|
|
ccnlSettings,
|
|
type User,
|
|
type UpsertUser,
|
|
type Guard,
|
|
type InsertGuard,
|
|
type Certification,
|
|
type InsertCertification,
|
|
type Vehicle,
|
|
type InsertVehicle,
|
|
type Customer,
|
|
type InsertCustomer,
|
|
type Site,
|
|
type InsertSite,
|
|
type Shift,
|
|
type InsertShift,
|
|
type ShiftAssignment,
|
|
type InsertShiftAssignment,
|
|
type Notification,
|
|
type InsertNotification,
|
|
type GuardConstraints,
|
|
type InsertGuardConstraints,
|
|
type SitePreference,
|
|
type InsertSitePreference,
|
|
type TrainingCourse,
|
|
type InsertTrainingCourse,
|
|
type Holiday,
|
|
type InsertHoliday,
|
|
type HolidayAssignment,
|
|
type InsertHolidayAssignment,
|
|
type Absence,
|
|
type InsertAbsence,
|
|
type AbsenceAffectedShift,
|
|
type InsertAbsenceAffectedShift,
|
|
type ContractParameters,
|
|
type InsertContractParameters,
|
|
type ServiceType,
|
|
type InsertServiceType,
|
|
type CcnlSetting,
|
|
type InsertCcnlSetting,
|
|
type GuardAvailability,
|
|
} from "@shared/schema";
|
|
import { db } from "./db";
|
|
import { eq, and, gte, lte, desc, or, sql as rawSql } from "drizzle-orm";
|
|
import { addDays, differenceInHours, parseISO, formatISO } from "date-fns";
|
|
|
|
export interface IStorage {
|
|
// User operations (Replit Auth required)
|
|
getUser(id: string): Promise<User | undefined>;
|
|
upsertUser(user: UpsertUser): Promise<User>;
|
|
getAllUsers(): Promise<User[]>;
|
|
updateUserRole(id: string, role: "admin" | "coordinator" | "guard" | "client"): Promise<User | undefined>;
|
|
|
|
// Guard operations
|
|
getAllGuards(): Promise<Guard[]>;
|
|
getGuard(id: string): Promise<Guard | undefined>;
|
|
createGuard(guard: InsertGuard): Promise<Guard>;
|
|
updateGuard(id: string, guard: Partial<InsertGuard>): Promise<Guard | undefined>;
|
|
|
|
// Certification operations
|
|
getCertificationsByGuard(guardId: string): Promise<Certification[]>;
|
|
createCertification(cert: InsertCertification): Promise<Certification>;
|
|
updateCertificationStatus(id: string, status: "valid" | "expiring_soon" | "expired"): Promise<void>;
|
|
|
|
// Service Type operations
|
|
getAllServiceTypes(): Promise<ServiceType[]>;
|
|
getServiceType(id: string): Promise<ServiceType | undefined>;
|
|
createServiceType(serviceType: InsertServiceType): Promise<ServiceType>;
|
|
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
|
|
deleteServiceType(id: string): Promise<ServiceType | undefined>;
|
|
|
|
// Customer operations
|
|
getAllCustomers(): Promise<Customer[]>;
|
|
getCustomer(id: string): Promise<Customer | undefined>;
|
|
createCustomer(customer: InsertCustomer): Promise<Customer>;
|
|
updateCustomer(id: string, customer: Partial<InsertCustomer>): Promise<Customer | undefined>;
|
|
deleteCustomer(id: string): Promise<Customer | undefined>;
|
|
|
|
// Site operations
|
|
getAllSites(): Promise<Site[]>;
|
|
getSite(id: string): Promise<Site | undefined>;
|
|
createSite(site: InsertSite): Promise<Site>;
|
|
updateSite(id: string, site: Partial<InsertSite>): Promise<Site | undefined>;
|
|
|
|
// Shift operations
|
|
getAllShifts(): Promise<Shift[]>;
|
|
getShift(id: string): Promise<Shift | undefined>;
|
|
getActiveShifts(): Promise<Shift[]>;
|
|
createShift(shift: InsertShift): Promise<Shift>;
|
|
updateShiftStatus(id: string, status: "planned" | "active" | "completed" | "cancelled"): Promise<void>;
|
|
|
|
// Shift Assignment operations
|
|
getShiftAssignments(shiftId: string): Promise<ShiftAssignment[]>;
|
|
createShiftAssignment(assignment: InsertShiftAssignment): Promise<ShiftAssignment>;
|
|
|
|
// Notification operations
|
|
getNotificationsByUser(userId: string): Promise<Notification[]>;
|
|
createNotification(notification: InsertNotification): Promise<Notification>;
|
|
markNotificationAsRead(id: string): Promise<void>;
|
|
|
|
// Guard Constraints operations
|
|
getGuardConstraints(guardId: string): Promise<GuardConstraints | undefined>;
|
|
upsertGuardConstraints(constraints: InsertGuardConstraints): Promise<GuardConstraints>;
|
|
|
|
// Site Preferences operations
|
|
getSitePreferences(siteId: string): Promise<SitePreference[]>;
|
|
createSitePreference(pref: InsertSitePreference): Promise<SitePreference>;
|
|
deleteSitePreference(id: string): Promise<void>;
|
|
|
|
// Training Courses operations
|
|
getTrainingCoursesByGuard(guardId: string): Promise<TrainingCourse[]>;
|
|
getAllTrainingCourses(): Promise<TrainingCourse[]>;
|
|
createTrainingCourse(course: InsertTrainingCourse): Promise<TrainingCourse>;
|
|
updateTrainingCourse(id: string, course: Partial<InsertTrainingCourse>): Promise<TrainingCourse | undefined>;
|
|
deleteTrainingCourse(id: string): Promise<void>;
|
|
|
|
// Holidays operations
|
|
getAllHolidays(year?: number): Promise<Holiday[]>;
|
|
createHoliday(holiday: InsertHoliday): Promise<Holiday>;
|
|
deleteHoliday(id: string): Promise<void>;
|
|
|
|
// Holiday Assignments operations
|
|
getHolidayAssignments(holidayId: string): Promise<HolidayAssignment[]>;
|
|
createHolidayAssignment(assignment: InsertHolidayAssignment): Promise<HolidayAssignment>;
|
|
deleteHolidayAssignment(id: string): Promise<void>;
|
|
|
|
// Absences operations
|
|
getAllAbsences(): Promise<Absence[]>;
|
|
getAbsencesByGuard(guardId: string): Promise<Absence[]>;
|
|
createAbsence(absence: InsertAbsence): Promise<Absence>;
|
|
updateAbsence(id: string, absence: Partial<InsertAbsence>): Promise<Absence | undefined>;
|
|
deleteAbsence(id: string): Promise<void>;
|
|
|
|
// Absence Affected Shifts operations
|
|
getAffectedShiftsByAbsence(absenceId: string): Promise<AbsenceAffectedShift[]>;
|
|
createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise<AbsenceAffectedShift>;
|
|
deleteAbsenceAffectedShift(id: string): Promise<void>;
|
|
|
|
// Contract Parameters operations
|
|
getContractParameters(): Promise<ContractParameters | undefined>;
|
|
createContractParameters(params: InsertContractParameters): Promise<ContractParameters>;
|
|
updateContractParameters(id: string, params: Partial<InsertContractParameters>): Promise<ContractParameters | undefined>;
|
|
|
|
// CCNL Settings operations
|
|
getAllCcnlSettings(): Promise<CcnlSetting[]>;
|
|
getCcnlSetting(key: string): Promise<CcnlSetting | undefined>;
|
|
upsertCcnlSetting(setting: InsertCcnlSetting): Promise<CcnlSetting>;
|
|
deleteCcnlSetting(key: string): Promise<void>;
|
|
|
|
// General Planning operations
|
|
getGuardsAvailability(
|
|
siteId: string,
|
|
location: string,
|
|
plannedStart: Date,
|
|
plannedEnd: Date
|
|
): Promise<GuardAvailability[]>;
|
|
|
|
// Shift Assignment operations with time slot management
|
|
deleteShiftAssignment(id: string): Promise<void>;
|
|
}
|
|
|
|
export class DatabaseStorage implements IStorage {
|
|
// User operations (Replit Auth required)
|
|
async getUser(id: string): Promise<User | undefined> {
|
|
const [user] = await db.select().from(users).where(eq(users.id, id));
|
|
return user;
|
|
}
|
|
|
|
async upsertUser(userData: UpsertUser): Promise<User> {
|
|
// Handle conflicts on both id (primary key) and email (unique constraint)
|
|
// Check if user exists by id or email first
|
|
const existingUser = await db
|
|
.select()
|
|
.from(users)
|
|
.where(
|
|
userData.id
|
|
? or(eq(users.id, userData.id), eq(users.email, userData.email || ''))
|
|
: eq(users.email, userData.email || '')
|
|
)
|
|
.limit(1);
|
|
|
|
if (existingUser.length > 0) {
|
|
// Update existing user - NEVER change the ID (it's a primary key)
|
|
const [updated] = await db
|
|
.update(users)
|
|
.set({
|
|
...(userData.email && { email: userData.email }),
|
|
...(userData.firstName && { firstName: userData.firstName }),
|
|
...(userData.lastName && { lastName: userData.lastName }),
|
|
...(userData.profileImageUrl && { profileImageUrl: userData.profileImageUrl }),
|
|
...(userData.role && { role: userData.role }),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(users.id, existingUser[0].id))
|
|
.returning();
|
|
return updated;
|
|
} else {
|
|
// Insert new user
|
|
const [user] = await db
|
|
.insert(users)
|
|
.values(userData)
|
|
.returning();
|
|
return user;
|
|
}
|
|
}
|
|
|
|
async getAllUsers(): Promise<User[]> {
|
|
return await db.select().from(users).orderBy(desc(users.createdAt));
|
|
}
|
|
|
|
async updateUserRole(id: string, role: "admin" | "coordinator" | "guard" | "client"): Promise<User | undefined> {
|
|
const [updated] = await db
|
|
.update(users)
|
|
.set({ role, updatedAt: new Date() })
|
|
.where(eq(users.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteUser(id: string): Promise<User | undefined> {
|
|
const [deleted] = await db.delete(users).where(eq(users.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Guard operations
|
|
async getAllGuards(): Promise<Guard[]> {
|
|
return await db.select().from(guards);
|
|
}
|
|
|
|
async getGuard(id: string): Promise<Guard | undefined> {
|
|
const [guard] = await db.select().from(guards).where(eq(guards.id, id));
|
|
return guard;
|
|
}
|
|
|
|
async createGuard(guard: InsertGuard): Promise<Guard> {
|
|
const [newGuard] = await db.insert(guards).values(guard).returning();
|
|
return newGuard;
|
|
}
|
|
|
|
async updateGuard(id: string, guardData: Partial<InsertGuard>): Promise<Guard | undefined> {
|
|
const [updated] = await db
|
|
.update(guards)
|
|
.set({ ...guardData, updatedAt: new Date() })
|
|
.where(eq(guards.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteGuard(id: string): Promise<Guard | undefined> {
|
|
const [deleted] = await db.delete(guards).where(eq(guards.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Vehicle operations
|
|
async getAllVehicles(): Promise<Vehicle[]> {
|
|
return await db.select().from(vehicles).orderBy(desc(vehicles.createdAt));
|
|
}
|
|
|
|
async getVehicle(id: string): Promise<Vehicle | undefined> {
|
|
const [vehicle] = await db.select().from(vehicles).where(eq(vehicles.id, id));
|
|
return vehicle;
|
|
}
|
|
|
|
async createVehicle(vehicle: InsertVehicle): Promise<Vehicle> {
|
|
const [newVehicle] = await db.insert(vehicles).values(vehicle).returning();
|
|
return newVehicle;
|
|
}
|
|
|
|
async updateVehicle(id: string, vehicleData: Partial<InsertVehicle>): Promise<Vehicle | undefined> {
|
|
const [updated] = await db
|
|
.update(vehicles)
|
|
.set({ ...vehicleData, updatedAt: new Date() })
|
|
.where(eq(vehicles.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteVehicle(id: string): Promise<Vehicle | undefined> {
|
|
const [deleted] = await db.delete(vehicles).where(eq(vehicles.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Certification operations
|
|
async getCertificationsByGuard(guardId: string): Promise<Certification[]> {
|
|
return await db
|
|
.select()
|
|
.from(certifications)
|
|
.where(eq(certifications.guardId, guardId))
|
|
.orderBy(desc(certifications.expiryDate));
|
|
}
|
|
|
|
async createCertification(cert: InsertCertification): Promise<Certification> {
|
|
const [newCert] = await db.insert(certifications).values(cert).returning();
|
|
return newCert;
|
|
}
|
|
|
|
async updateCertificationStatus(
|
|
id: string,
|
|
status: "valid" | "expiring_soon" | "expired"
|
|
): Promise<void> {
|
|
await db
|
|
.update(certifications)
|
|
.set({ status })
|
|
.where(eq(certifications.id, id));
|
|
}
|
|
|
|
// Service Type operations
|
|
async getAllServiceTypes(): Promise<ServiceType[]> {
|
|
return await db.select().from(serviceTypes).orderBy(desc(serviceTypes.createdAt));
|
|
}
|
|
|
|
async getServiceType(id: string): Promise<ServiceType | undefined> {
|
|
const [serviceType] = await db.select().from(serviceTypes).where(eq(serviceTypes.id, id));
|
|
return serviceType;
|
|
}
|
|
|
|
async createServiceType(serviceType: InsertServiceType): Promise<ServiceType> {
|
|
const [newServiceType] = await db.insert(serviceTypes).values(serviceType).returning();
|
|
return newServiceType;
|
|
}
|
|
|
|
async updateServiceType(id: string, serviceTypeData: Partial<InsertServiceType>): Promise<ServiceType | undefined> {
|
|
const [updated] = await db
|
|
.update(serviceTypes)
|
|
.set({ ...serviceTypeData, updatedAt: new Date() })
|
|
.where(eq(serviceTypes.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteServiceType(id: string): Promise<ServiceType | undefined> {
|
|
const [deleted] = await db.delete(serviceTypes).where(eq(serviceTypes.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Customer operations
|
|
async getAllCustomers(): Promise<Customer[]> {
|
|
return await db.select().from(customers).orderBy(desc(customers.createdAt));
|
|
}
|
|
|
|
async getCustomer(id: string): Promise<Customer | undefined> {
|
|
const [customer] = await db.select().from(customers).where(eq(customers.id, id));
|
|
return customer;
|
|
}
|
|
|
|
async createCustomer(customer: InsertCustomer): Promise<Customer> {
|
|
const [newCustomer] = await db.insert(customers).values(customer).returning();
|
|
return newCustomer;
|
|
}
|
|
|
|
async updateCustomer(id: string, customerData: Partial<InsertCustomer>): Promise<Customer | undefined> {
|
|
const [updated] = await db
|
|
.update(customers)
|
|
.set({ ...customerData, updatedAt: new Date() })
|
|
.where(eq(customers.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteCustomer(id: string): Promise<Customer | undefined> {
|
|
const [deleted] = await db.delete(customers).where(eq(customers.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Site operations
|
|
async getAllSites(): Promise<Site[]> {
|
|
return await db.select().from(sites);
|
|
}
|
|
|
|
async getSite(id: string): Promise<Site | undefined> {
|
|
const [site] = await db.select().from(sites).where(eq(sites.id, id));
|
|
return site;
|
|
}
|
|
|
|
async createSite(site: InsertSite): Promise<Site> {
|
|
const [newSite] = await db.insert(sites).values(site).returning();
|
|
return newSite;
|
|
}
|
|
|
|
async updateSite(id: string, siteData: Partial<InsertSite>): Promise<Site | undefined> {
|
|
const [updated] = await db
|
|
.update(sites)
|
|
.set({ ...siteData, updatedAt: new Date() })
|
|
.where(eq(sites.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteSite(id: string): Promise<Site | undefined> {
|
|
const [deleted] = await db.delete(sites).where(eq(sites.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Shift operations
|
|
async getAllShifts(): Promise<Shift[]> {
|
|
return await db.select().from(shifts).orderBy(desc(shifts.startTime));
|
|
}
|
|
|
|
async getShift(id: string): Promise<Shift | undefined> {
|
|
const [shift] = await db.select().from(shifts).where(eq(shifts.id, id));
|
|
return shift;
|
|
}
|
|
|
|
async getActiveShifts(): Promise<Shift[]> {
|
|
return await db
|
|
.select()
|
|
.from(shifts)
|
|
.where(eq(shifts.status, "active"))
|
|
.orderBy(desc(shifts.startTime));
|
|
}
|
|
|
|
async createShift(shift: InsertShift): Promise<Shift> {
|
|
const [newShift] = await db.insert(shifts).values(shift).returning();
|
|
return newShift;
|
|
}
|
|
|
|
async updateShiftStatus(
|
|
id: string,
|
|
status: "planned" | "active" | "completed" | "cancelled"
|
|
): Promise<void> {
|
|
await db
|
|
.update(shifts)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(eq(shifts.id, id));
|
|
}
|
|
|
|
async updateShift(id: string, shiftData: Partial<InsertShift>): Promise<Shift | undefined> {
|
|
const [updated] = await db
|
|
.update(shifts)
|
|
.set({ ...shiftData, updatedAt: new Date() })
|
|
.where(eq(shifts.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteShift(id: string): Promise<Shift | undefined> {
|
|
const [deleted] = await db.delete(shifts).where(eq(shifts.id, id)).returning();
|
|
return deleted;
|
|
}
|
|
|
|
// Shift Assignment operations
|
|
async getShiftAssignments(shiftId: string): Promise<ShiftAssignment[]> {
|
|
return await db
|
|
.select()
|
|
.from(shiftAssignments)
|
|
.where(eq(shiftAssignments.shiftId, shiftId));
|
|
}
|
|
|
|
async createShiftAssignment(assignment: InsertShiftAssignment): Promise<ShiftAssignment> {
|
|
const [newAssignment] = await db
|
|
.insert(shiftAssignments)
|
|
.values(assignment)
|
|
.returning();
|
|
return newAssignment;
|
|
}
|
|
|
|
async deleteShiftAssignment(id: string): Promise<void> {
|
|
await db
|
|
.delete(shiftAssignments)
|
|
.where(eq(shiftAssignments.id, id));
|
|
}
|
|
|
|
// Notification operations
|
|
async getNotificationsByUser(userId: string): Promise<Notification[]> {
|
|
return await db
|
|
.select()
|
|
.from(notifications)
|
|
.where(eq(notifications.userId, userId))
|
|
.orderBy(desc(notifications.createdAt));
|
|
}
|
|
|
|
async createNotification(notification: InsertNotification): Promise<Notification> {
|
|
const [newNotification] = await db
|
|
.insert(notifications)
|
|
.values(notification)
|
|
.returning();
|
|
return newNotification;
|
|
}
|
|
|
|
async markNotificationAsRead(id: string): Promise<void> {
|
|
await db
|
|
.update(notifications)
|
|
.set({ isRead: true })
|
|
.where(eq(notifications.id, id));
|
|
}
|
|
|
|
// Guard Constraints operations
|
|
async getGuardConstraints(guardId: string): Promise<GuardConstraints | undefined> {
|
|
const [constraints] = await db
|
|
.select()
|
|
.from(guardConstraints)
|
|
.where(eq(guardConstraints.guardId, guardId));
|
|
return constraints;
|
|
}
|
|
|
|
async upsertGuardConstraints(constraintsData: InsertGuardConstraints): Promise<GuardConstraints> {
|
|
const existing = await this.getGuardConstraints(constraintsData.guardId);
|
|
|
|
if (existing) {
|
|
const [updated] = await db
|
|
.update(guardConstraints)
|
|
.set({ ...constraintsData, updatedAt: new Date() })
|
|
.where(eq(guardConstraints.guardId, constraintsData.guardId))
|
|
.returning();
|
|
return updated;
|
|
} else {
|
|
const [created] = await db
|
|
.insert(guardConstraints)
|
|
.values(constraintsData)
|
|
.returning();
|
|
return created;
|
|
}
|
|
}
|
|
|
|
// Site Preferences operations
|
|
async getSitePreferences(siteId: string): Promise<SitePreference[]> {
|
|
return await db
|
|
.select()
|
|
.from(sitePreferences)
|
|
.where(eq(sitePreferences.siteId, siteId));
|
|
}
|
|
|
|
async createSitePreference(pref: InsertSitePreference): Promise<SitePreference> {
|
|
const [newPref] = await db.insert(sitePreferences).values(pref).returning();
|
|
return newPref;
|
|
}
|
|
|
|
async deleteSitePreference(id: string): Promise<void> {
|
|
await db.delete(sitePreferences).where(eq(sitePreferences.id, id));
|
|
}
|
|
|
|
// Training Courses operations
|
|
async getTrainingCoursesByGuard(guardId: string): Promise<TrainingCourse[]> {
|
|
return await db
|
|
.select()
|
|
.from(trainingCourses)
|
|
.where(eq(trainingCourses.guardId, guardId))
|
|
.orderBy(desc(trainingCourses.scheduledDate));
|
|
}
|
|
|
|
async getAllTrainingCourses(): Promise<TrainingCourse[]> {
|
|
return await db.select().from(trainingCourses).orderBy(desc(trainingCourses.scheduledDate));
|
|
}
|
|
|
|
async createTrainingCourse(course: InsertTrainingCourse): Promise<TrainingCourse> {
|
|
const [newCourse] = await db.insert(trainingCourses).values(course).returning();
|
|
return newCourse;
|
|
}
|
|
|
|
async updateTrainingCourse(id: string, courseData: Partial<InsertTrainingCourse>): Promise<TrainingCourse | undefined> {
|
|
const [updated] = await db
|
|
.update(trainingCourses)
|
|
.set(courseData)
|
|
.where(eq(trainingCourses.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteTrainingCourse(id: string): Promise<void> {
|
|
await db.delete(trainingCourses).where(eq(trainingCourses.id, id));
|
|
}
|
|
|
|
// Holidays operations
|
|
async getAllHolidays(year?: number): Promise<Holiday[]> {
|
|
if (year) {
|
|
return await db
|
|
.select()
|
|
.from(holidays)
|
|
.where(eq(holidays.year, year))
|
|
.orderBy(holidays.date);
|
|
}
|
|
return await db.select().from(holidays).orderBy(holidays.date);
|
|
}
|
|
|
|
async createHoliday(holiday: InsertHoliday): Promise<Holiday> {
|
|
const [newHoliday] = await db.insert(holidays).values(holiday).returning();
|
|
return newHoliday;
|
|
}
|
|
|
|
async deleteHoliday(id: string): Promise<void> {
|
|
await db.delete(holidays).where(eq(holidays.id, id));
|
|
}
|
|
|
|
// Holiday Assignments operations
|
|
async getHolidayAssignments(holidayId: string): Promise<HolidayAssignment[]> {
|
|
return await db
|
|
.select()
|
|
.from(holidayAssignments)
|
|
.where(eq(holidayAssignments.holidayId, holidayId));
|
|
}
|
|
|
|
async createHolidayAssignment(assignment: InsertHolidayAssignment): Promise<HolidayAssignment> {
|
|
const [newAssignment] = await db.insert(holidayAssignments).values(assignment).returning();
|
|
return newAssignment;
|
|
}
|
|
|
|
async deleteHolidayAssignment(id: string): Promise<void> {
|
|
await db.delete(holidayAssignments).where(eq(holidayAssignments.id, id));
|
|
}
|
|
|
|
// Absences operations
|
|
async getAllAbsences(): Promise<Absence[]> {
|
|
return await db.select().from(absences).orderBy(desc(absences.startDate));
|
|
}
|
|
|
|
async getAbsencesByGuard(guardId: string): Promise<Absence[]> {
|
|
return await db
|
|
.select()
|
|
.from(absences)
|
|
.where(eq(absences.guardId, guardId))
|
|
.orderBy(desc(absences.startDate));
|
|
}
|
|
|
|
async createAbsence(absence: InsertAbsence): Promise<Absence> {
|
|
const [newAbsence] = await db.insert(absences).values(absence).returning();
|
|
return newAbsence;
|
|
}
|
|
|
|
async updateAbsence(id: string, absenceData: Partial<InsertAbsence>): Promise<Absence | undefined> {
|
|
const [updated] = await db
|
|
.update(absences)
|
|
.set(absenceData)
|
|
.where(eq(absences.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
async deleteAbsence(id: string): Promise<void> {
|
|
await db.delete(absences).where(eq(absences.id, id));
|
|
}
|
|
|
|
// Absence Affected Shifts operations
|
|
async getAffectedShiftsByAbsence(absenceId: string): Promise<AbsenceAffectedShift[]> {
|
|
return await db
|
|
.select()
|
|
.from(absenceAffectedShifts)
|
|
.where(eq(absenceAffectedShifts.absenceId, absenceId));
|
|
}
|
|
|
|
async createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise<AbsenceAffectedShift> {
|
|
const [newAffected] = await db.insert(absenceAffectedShifts).values(affected).returning();
|
|
return newAffected;
|
|
}
|
|
|
|
async deleteAbsenceAffectedShift(id: string): Promise<void> {
|
|
await db.delete(absenceAffectedShifts).where(eq(absenceAffectedShifts.id, id));
|
|
}
|
|
|
|
// Contract Parameters operations
|
|
async getContractParameters(): Promise<ContractParameters | undefined> {
|
|
const params = await db.select().from(contractParameters).limit(1);
|
|
return params[0];
|
|
}
|
|
|
|
async createContractParameters(params: InsertContractParameters): Promise<ContractParameters> {
|
|
const [newParams] = await db.insert(contractParameters).values(params).returning();
|
|
return newParams;
|
|
}
|
|
|
|
async updateContractParameters(id: string, params: Partial<InsertContractParameters>): Promise<ContractParameters | undefined> {
|
|
const [updated] = await db
|
|
.update(contractParameters)
|
|
.set(params)
|
|
.where(eq(contractParameters.id, id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
// CCNL Settings operations
|
|
async getAllCcnlSettings(): Promise<CcnlSetting[]> {
|
|
return await db.select().from(ccnlSettings);
|
|
}
|
|
|
|
async getCcnlSetting(key: string): Promise<CcnlSetting | undefined> {
|
|
const [setting] = await db.select().from(ccnlSettings).where(eq(ccnlSettings.key, key));
|
|
return setting;
|
|
}
|
|
|
|
async upsertCcnlSetting(setting: InsertCcnlSetting): Promise<CcnlSetting> {
|
|
const existing = await this.getCcnlSetting(setting.key);
|
|
|
|
if (existing) {
|
|
const [updated] = await db
|
|
.update(ccnlSettings)
|
|
.set({ ...setting, updatedAt: new Date() })
|
|
.where(eq(ccnlSettings.key, setting.key))
|
|
.returning();
|
|
return updated;
|
|
} else {
|
|
const [newSetting] = await db.insert(ccnlSettings).values(setting).returning();
|
|
return newSetting;
|
|
}
|
|
}
|
|
|
|
async deleteCcnlSetting(key: string): Promise<void> {
|
|
await db.delete(ccnlSettings).where(eq(ccnlSettings.key, key));
|
|
}
|
|
|
|
// General Planning operations with time slot conflict detection
|
|
async getGuardsAvailability(
|
|
siteId: string,
|
|
location: string,
|
|
plannedStart: Date,
|
|
plannedEnd: Date
|
|
): Promise<GuardAvailability[]> {
|
|
// Helper: Check if two time ranges overlap
|
|
const hasOverlap = (start1: Date, end1: Date, start2: Date, end2: Date): boolean => {
|
|
return start1 < end2 && end1 > start2;
|
|
};
|
|
|
|
// Helper: Calculate night hours (22:00-06:00)
|
|
const calculateNightHours = (start: Date, end: Date): number => {
|
|
let nightHours = 0;
|
|
const current = new Date(start);
|
|
|
|
while (current < end) {
|
|
const hour = current.getUTCHours();
|
|
if (hour >= 22 || hour < 6) {
|
|
nightHours += 1;
|
|
}
|
|
current.setHours(current.getHours() + 1);
|
|
}
|
|
|
|
return nightHours;
|
|
};
|
|
|
|
// Calculate week boundaries for weekly hours calculation
|
|
const weekStart = new Date(plannedStart);
|
|
weekStart.setDate(plannedStart.getDate() - plannedStart.getDay() + (plannedStart.getDay() === 0 ? -6 : 1));
|
|
weekStart.setHours(0, 0, 0, 0);
|
|
const weekEnd = addDays(weekStart, 6);
|
|
weekEnd.setHours(23, 59, 59, 999);
|
|
|
|
// Get contract parameters
|
|
let contractParams = await this.getContractParameters();
|
|
if (!contractParams) {
|
|
contractParams = await this.createContractParameters({
|
|
contractType: "CCNL_VIGILANZA_2024",
|
|
});
|
|
}
|
|
|
|
const maxOrdinaryHours = contractParams.maxHoursPerWeek || 40; // 40h
|
|
const maxOvertimeHours = contractParams.maxOvertimePerWeek || 8; // 8h
|
|
const maxTotalHours = maxOrdinaryHours + maxOvertimeHours; // 48h
|
|
const maxNightHours = contractParams.maxNightHoursPerWeek || 48; // 48h
|
|
const minDailyRest = contractParams.minDailyRestHours || 11; // 11h
|
|
|
|
// Get site to check requirements
|
|
const site = await this.getSite(siteId);
|
|
if (!site) {
|
|
return [];
|
|
}
|
|
|
|
// Get all guards from the same location
|
|
const allGuards = await db
|
|
.select()
|
|
.from(guards)
|
|
.where(eq(guards.location, location as any));
|
|
|
|
// Filter guards by site requirements
|
|
const eligibleGuards = allGuards.filter((guard: Guard) => {
|
|
if (site.requiresArmed && !guard.isArmed) return false;
|
|
if (site.requiresDriverLicense && !guard.hasDriverLicense) return false;
|
|
return true;
|
|
});
|
|
|
|
const requestedHours = differenceInHours(plannedEnd, plannedStart);
|
|
const requestedNightHours = calculateNightHours(plannedStart, plannedEnd);
|
|
|
|
// Analyze each guard's availability
|
|
const guardsWithAvailability: GuardAvailability[] = [];
|
|
|
|
for (const guard of eligibleGuards) {
|
|
// Get all shift assignments for this guard in the week (for weekly hours)
|
|
const weeklyAssignments = await db
|
|
.select({
|
|
id: shiftAssignments.id,
|
|
shiftId: shiftAssignments.shiftId,
|
|
plannedStartTime: shiftAssignments.plannedStartTime,
|
|
plannedEndTime: shiftAssignments.plannedEndTime,
|
|
})
|
|
.from(shiftAssignments)
|
|
.where(
|
|
and(
|
|
eq(shiftAssignments.guardId, guard.id),
|
|
gte(shiftAssignments.plannedStartTime, weekStart),
|
|
lte(shiftAssignments.plannedStartTime, weekEnd)
|
|
)
|
|
)
|
|
.orderBy(desc(shiftAssignments.plannedEndTime));
|
|
|
|
// Calculate total weekly hours and night hours assigned
|
|
let weeklyHoursAssigned = 0;
|
|
let nightHoursAssigned = 0;
|
|
let lastShiftEnd: Date | null = null;
|
|
|
|
for (const assignment of weeklyAssignments) {
|
|
const hours = differenceInHours(assignment.plannedEndTime, assignment.plannedStartTime);
|
|
weeklyHoursAssigned += hours;
|
|
nightHoursAssigned += calculateNightHours(assignment.plannedStartTime, assignment.plannedEndTime);
|
|
|
|
// Track last shift end for rest calculation
|
|
if (!lastShiftEnd || assignment.plannedEndTime > lastShiftEnd) {
|
|
lastShiftEnd = assignment.plannedEndTime;
|
|
}
|
|
}
|
|
|
|
// Calculate ordinary and overtime hours
|
|
const ordinaryHoursAssigned = Math.min(weeklyHoursAssigned, maxOrdinaryHours);
|
|
const overtimeHoursAssigned = Math.max(0, weeklyHoursAssigned - maxOrdinaryHours);
|
|
const ordinaryHoursRemaining = Math.max(0, maxOrdinaryHours - weeklyHoursAssigned);
|
|
const overtimeHoursRemaining = Math.max(0, maxOvertimeHours - overtimeHoursAssigned);
|
|
const weeklyHoursRemaining = ordinaryHoursRemaining + overtimeHoursRemaining;
|
|
|
|
// Check if shift requires overtime
|
|
const requiresOvertime = requestedHours > ordinaryHoursRemaining;
|
|
|
|
// Check for time conflicts with the requested slot
|
|
const conflicts = [];
|
|
const reasons: string[] = [];
|
|
|
|
for (const assignment of weeklyAssignments) {
|
|
if (hasOverlap(plannedStart, plannedEnd, assignment.plannedStartTime, assignment.plannedEndTime)) {
|
|
// Get site name for conflict
|
|
const [shift] = await db
|
|
.select({ siteName: sites.name })
|
|
.from(shifts)
|
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
|
.where(eq(shifts.id, assignment.shiftId));
|
|
|
|
conflicts.push({
|
|
from: assignment.plannedStartTime,
|
|
to: assignment.plannedEndTime,
|
|
siteName: shift?.siteName || 'Sito sconosciuto',
|
|
shiftId: assignment.shiftId,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check rest violation (11h between shifts)
|
|
let hasRestViolation = false;
|
|
if (lastShiftEnd) {
|
|
const hoursSinceLastShift = differenceInHours(plannedStart, lastShiftEnd);
|
|
if (hoursSinceLastShift < minDailyRest) {
|
|
hasRestViolation = true;
|
|
reasons.push(`Riposo insufficiente (${Math.max(0, hoursSinceLastShift).toFixed(1)}h dall'ultimo turno, minimo ${minDailyRest}h)`);
|
|
}
|
|
}
|
|
|
|
// Determine availability
|
|
let isAvailable = true;
|
|
|
|
// EXCLUDE guards already assigned on same day/time
|
|
if (conflicts.length > 0) {
|
|
isAvailable = false;
|
|
reasons.push(`Già assegnata in ${conflicts.length} turno/i nello stesso orario`);
|
|
}
|
|
|
|
// Check if enough hours available (total)
|
|
if (weeklyHoursRemaining < requestedHours) {
|
|
isAvailable = false;
|
|
reasons.push(`Ore settimanali insufficienti (${Math.max(0, weeklyHoursRemaining)}h disponibili, ${requestedHours}h richieste)`);
|
|
}
|
|
|
|
// Check night hours limit
|
|
if (nightHoursAssigned + requestedNightHours > maxNightHours) {
|
|
isAvailable = false;
|
|
reasons.push(`Ore notturne esaurite (${nightHoursAssigned}h lavorate, max ${maxNightHours}h/settimana)`);
|
|
}
|
|
|
|
// Rest violation makes guard unavailable
|
|
if (hasRestViolation) {
|
|
isAvailable = false;
|
|
}
|
|
|
|
// Build guard name from new fields
|
|
const guardName = guard.firstName && guard.lastName
|
|
? `${guard.firstName} ${guard.lastName}`
|
|
: guard.badgeNumber;
|
|
|
|
guardsWithAvailability.push({
|
|
guardId: guard.id,
|
|
guardName,
|
|
badgeNumber: guard.badgeNumber,
|
|
weeklyHoursRemaining,
|
|
weeklyHoursAssigned,
|
|
weeklyHoursMax: maxTotalHours,
|
|
ordinaryHoursRemaining,
|
|
overtimeHoursRemaining,
|
|
nightHoursAssigned,
|
|
requiresOvertime,
|
|
hasRestViolation,
|
|
lastShiftEnd,
|
|
isAvailable,
|
|
conflicts,
|
|
unavailabilityReasons: reasons,
|
|
});
|
|
}
|
|
|
|
// Sort: available with ordinary hours first, then overtime, then unavailable
|
|
guardsWithAvailability.sort((a, b) => {
|
|
if (a.isAvailable && !b.isAvailable) return -1;
|
|
if (!a.isAvailable && b.isAvailable) return 1;
|
|
if (a.isAvailable && b.isAvailable) {
|
|
if (!a.requiresOvertime && b.requiresOvertime) return -1;
|
|
if (a.requiresOvertime && !b.requiresOvertime) return 1;
|
|
}
|
|
return b.weeklyHoursRemaining - a.weeklyHoursRemaining;
|
|
});
|
|
|
|
return guardsWithAvailability;
|
|
}
|
|
}
|
|
|
|
export const storage = new DatabaseStorage();
|