From dcbf059d73d1b071c5b353dece9bb3fc07341410 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 11 Oct 2025 19:30:16 +0000 Subject: [PATCH] Add advanced planning features with new database schemas and UI Implement new API endpoints and database schemas for guard constraints, site preferences, training courses, holidays, and absences, alongside a dedicated advanced planning UI. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/H8Wilyj --- client/src/App.tsx | 2 + client/src/components/app-sidebar.tsx | 7 + client/src/pages/planning.tsx | 480 ++++++++++++++++++++++++++ replit.md | 80 +++++ server/routes.ts | 178 ++++++++++ server/storage.ts | 221 ++++++++++++ 6 files changed, 968 insertions(+) create mode 100644 client/src/pages/planning.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index cfbefa7..a9b9d26 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,6 +16,7 @@ import Shifts from "@/pages/shifts"; import Reports from "@/pages/reports"; import Notifications from "@/pages/notifications"; import Users from "@/pages/users"; +import Planning from "@/pages/planning"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -30,6 +31,7 @@ function Router() { + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index 4736c54..5fb3e2d 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -8,6 +8,7 @@ import { Settings, LogOut, UserCog, + ClipboardList, } from "lucide-react"; import { Link, useLocation } from "wouter"; import { @@ -40,6 +41,12 @@ const menuItems = [ icon: Calendar, roles: ["admin", "coordinator", "guard"], }, + { + title: "Pianificazione", + url: "/planning", + icon: ClipboardList, + roles: ["admin", "coordinator"], + }, { title: "Guardie", url: "/guards", diff --git a/client/src/pages/planning.tsx b/client/src/pages/planning.tsx new file mode 100644 index 0000000..b7f6202 --- /dev/null +++ b/client/src/pages/planning.tsx @@ -0,0 +1,480 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { queryClient, apiRequest } from "@/lib/queryClient"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useToast } from "@/hooks/use-toast"; +import { Calendar, GraduationCap, HeartPulse, PartyPopper, Plus, Trash2 } from "lucide-react"; +import { format } from "date-fns"; +import { it } from "date-fns/locale"; + +export default function PlanningPage() { + const { toast } = useToast(); + const [activeTab, setActiveTab] = useState("training"); + + return ( +
+
+
+

Pianificazione Avanzata

+

+ Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni +

+
+
+ + + + + + Formazione + + + + Assenze + + + + Festività + + + + + + + + + + + + + + + +
+ ); +} + +function TrainingTab() { + const { toast } = useToast(); + const [isCreateOpen, setIsCreateOpen] = useState(false); + + const { data: courses = [], isLoading } = useQuery({ + queryKey: ["/api/training-courses"], + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest("/api/training-courses", "POST", data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] }); + toast({ title: "Corso creato con successo" }); + setIsCreateOpen(false); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => apiRequest(`/api/training-courses/${id}`, "DELETE"), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] }); + toast({ title: "Corso eliminato" }); + }, + }); + + if (isLoading) { + return
Caricamento corsi...
; + } + + return ( +
+
+ + + + + + + Nuovo Corso di Formazione + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + createMutation.mutate({ + guardId: formData.get("guardId"), + courseName: formData.get("courseName"), + courseType: formData.get("courseType"), + scheduledDate: formData.get("scheduledDate"), + status: "scheduled", + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {courses.map((course: any) => ( + + +
+
+ {course.courseName} + + Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"} + +
+
+ + {course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"} + + + {course.status} + + +
+
+
+
+ ))} + {courses.length === 0 && ( +
+ Nessun corso di formazione programmato +
+ )} +
+
+ ); +} + +function AbsencesTab() { + const { toast } = useToast(); + const [isCreateOpen, setIsCreateOpen] = useState(false); + + const { data: absences = [], isLoading } = useQuery({ + queryKey: ["/api/absences"], + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest("/api/absences", "POST", data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/absences"] }); + toast({ title: "Assenza registrata" }); + setIsCreateOpen(false); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => apiRequest(`/api/absences/${id}`, "DELETE"), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/absences"] }); + toast({ title: "Assenza eliminata" }); + }, + }); + + if (isLoading) { + return
Caricamento assenze...
; + } + + return ( +
+
+ + + + + + + Registra Assenza + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + createMutation.mutate({ + guardId: formData.get("guardId"), + type: formData.get("type"), + startDate: formData.get("startDate"), + endDate: formData.get("endDate"), + notes: formData.get("notes"), + isApproved: false, + needsSubstitute: true, + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {absences.map((absence: any) => ( + + +
+
+ + {absence.type === "sick_leave" && "Malattia"} + {absence.type === "vacation" && "Ferie"} + {absence.type === "personal_leave" && "Permesso"} + {absence.type === "injury" && "Infortunio"} + + + Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "} + {format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })} + +
+
+ + {absence.isApproved ? "Approvata" : "In attesa"} + + +
+
+
+
+ ))} + {absences.length === 0 && ( +
+ Nessuna assenza registrata +
+ )} +
+
+ ); +} + +function HolidaysTab() { + const { toast } = useToast(); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const currentYear = new Date().getFullYear(); + + const { data: holidays = [], isLoading } = useQuery({ + queryKey: ["/api/holidays", currentYear], + queryFn: () => fetch(`/api/holidays?year=${currentYear}`).then((r) => r.json()), + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest("/api/holidays", "POST", data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] }); + toast({ title: "Festività creata" }); + setIsCreateOpen(false); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => apiRequest(`/api/holidays/${id}`, "DELETE"), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] }); + toast({ title: "Festività eliminata" }); + }, + }); + + if (isLoading) { + return
Caricamento festività...
; + } + + return ( +
+
+ + + + + + + Nuova Festività + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const dateValue = formData.get("date") as string; + createMutation.mutate({ + name: formData.get("name"), + date: dateValue, + year: new Date(dateValue).getFullYear(), + isNational: formData.get("isNational") === "true", + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {holidays.map((holiday: any) => ( + + +
+
+ {holiday.name} + + {format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })} + +
+
+ + {holiday.isNational ? "Nazionale" : "Regionale"} + + +
+
+
+
+ ))} + {holidays.length === 0 && ( +
+ Nessuna festività configurata per {currentYear} +
+ )} +
+
+ ); +} diff --git a/replit.md b/replit.md index ab159b5..69e232c 100644 --- a/replit.md +++ b/replit.md @@ -70,6 +70,56 @@ Sistema professionale di gestione turni 24/7 per istituti di vigilanza con: - id, userId (FK), title, message, type - isRead, relatedEntityId +### Scheduling & Constraints Tables + +**guard_constraints** (Vincoli Operatori): +- id, guardId (FK unique) +- preferredShiftType: morning | afternoon | night | any +- maxHoursPerDay, maxHoursPerWeek +- preferredDaysOff: array +- availableOnHolidays: boolean + +**site_preferences** (Preferenze Siti): +- id, siteId (FK), guardId (FK) +- preference: preferred | blacklisted +- priority, reason +- Unique constraint: (siteId, guardId) + +**contract_parameters** (Parametri CCNL): +- contractType (unique, default: CCNL_VIGILANZA_2023) +- maxHoursPerDay (8), maxOvertimePerDay (2) +- maxHoursPerWeek (40), maxOvertimePerWeek (8) +- minDailyRestHours (11), minWeeklyRestHours (24) +- maxNightHoursPerWeek (48) +- Maggiorazioni: holidayPayIncrease (30%), nightPayIncrease (20%), overtimePayIncrease (15%) + +**training_courses** (Formazione): +- id, guardId (FK) +- courseName, courseType (mandatory/optional) +- scheduledDate, completionDate, expiryDate +- status: scheduled | completed | expired | cancelled +- provider, hours, certificateUrl + +**holidays** (Festività): +- id, name, date, year +- isNational: boolean +- Unique constraint: (date, year) + +**holiday_assignments** (Rotazioni Festività): +- id, holidayId (FK), guardId (FK), shiftId (FK nullable) +- Unique constraint: (holidayId, guardId) + +**absences** (Assenze/Malattie): +- id, guardId (FK), type: sick_leave | vacation | personal_leave | injury +- startDate, endDate +- isApproved, needsSubstitute, substituteGuardId (FK nullable) +- certificateUrl, notes + +**absence_affected_shifts** (Turni Impattati da Assenza): +- id, absenceId (FK), shiftId (FK) +- isSubstituted: boolean +- Unique constraint: (absenceId, shiftId) + ## API Endpoints ### Authentication @@ -230,6 +280,36 @@ All interactive elements have `data-testid` attributes for automated testing. - Toast notifiche successo/errore - Auto-close dialog dopo aggiornamento - Test e2e passati per tutte le pagine ✅ +- **Estensione Schema Database per Pianificazione Avanzata** ✅: + - **guard_constraints**: vincoli operatori (preferenze turno, max ore, riposi, disponibilità festività) + - **site_preferences**: continuità servizio (operatori preferiti/blacklisted per sito) + - **contract_parameters**: parametri CCNL (limiti orari, riposi, maggiorazioni) + - **training_courses**: formazione obbligatoria con scadenze e tracking + - **holidays + holiday_assignments**: festività nazionali con rotazioni operatori + - **absences + absence_affected_shifts**: assenze/malattie con sistema sostituzione automatica + - Tutti unique indexes e FK integrity implementati per garantire coerenza dati + - Schema pronto per algoritmo pianificazione automatica turni +- **Pagina Pianificazione Avanzata** ✅: + - Rotta /planning accessibile a admin e coordinator + - UI con 3 tabs: Formazione, Assenze, Festività + - CRUD completo per training courses (mandatory/optional, scheduled/completed) + - CRUD completo per absences (sick_leave, vacation, personal_leave, injury) + - CRUD completo per holidays con filtraggio per anno + - Dialogs di creazione con validazione form + - Fix cache invalidation bug: TanStack Query ora invalida correttamente con parametri year + - Data formatting italiano con date-fns locale + - Toast notifications per feedback operazioni +- **API Routes Pianificazione** ✅: + - GET/POST /api/guard-constraints/:guardId - vincoli operatore (upsert) + - GET/POST/DELETE /api/site-preferences - preferenze sito-guardia + - GET/POST/PATCH/DELETE /api/training-courses - formazione (con filtro guardId) + - GET/POST/DELETE /api/holidays - festività (con filtro year) + - GET/POST/PATCH/DELETE /api/absences - assenze (con filtro guardId) +- **Storage Layer Completo** ✅: + - DatabaseStorage esteso con metodi CRUD per tutte le nuove entità + - Guard constraints: upsert semantics (create o update se esiste) + - Gestione relazioni FK con cascading deletes dove appropriato + - Query con ordering e filtering per date/status - Aggiunto SEO completo (title, meta description, Open Graph) - Tutti i componenti testabili con data-testid attributes diff --git a/server/routes.ts b/server/routes.ts index 683d24d..10659cc 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -426,6 +426,184 @@ export async function registerRoutes(app: Express): Promise { } }); + // ============= GUARD CONSTRAINTS ROUTES ============= + app.get("/api/guard-constraints/:guardId", isAuthenticated, async (req, res) => { + try { + const constraints = await storage.getGuardConstraints(req.params.guardId); + res.json(constraints || null); + } catch (error) { + console.error("Error fetching guard constraints:", error); + res.status(500).json({ message: "Failed to fetch guard constraints" }); + } + }); + + app.post("/api/guard-constraints", isAuthenticated, async (req, res) => { + try { + const constraints = await storage.upsertGuardConstraints(req.body); + res.json(constraints); + } catch (error) { + console.error("Error upserting guard constraints:", error); + res.status(500).json({ message: "Failed to save guard constraints" }); + } + }); + + // ============= SITE PREFERENCES ROUTES ============= + app.get("/api/site-preferences/:siteId", isAuthenticated, async (req, res) => { + try { + const preferences = await storage.getSitePreferences(req.params.siteId); + res.json(preferences); + } catch (error) { + console.error("Error fetching site preferences:", error); + res.status(500).json({ message: "Failed to fetch site preferences" }); + } + }); + + app.post("/api/site-preferences", isAuthenticated, async (req, res) => { + try { + const preference = await storage.createSitePreference(req.body); + res.json(preference); + } catch (error) { + console.error("Error creating site preference:", error); + res.status(500).json({ message: "Failed to create site preference" }); + } + }); + + app.delete("/api/site-preferences/:id", isAuthenticated, async (req, res) => { + try { + await storage.deleteSitePreference(req.params.id); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting site preference:", error); + res.status(500).json({ message: "Failed to delete site preference" }); + } + }); + + // ============= TRAINING COURSES ROUTES ============= + app.get("/api/training-courses", isAuthenticated, async (req, res) => { + try { + const guardId = req.query.guardId as string | undefined; + const courses = guardId + ? await storage.getTrainingCoursesByGuard(guardId) + : await storage.getAllTrainingCourses(); + res.json(courses); + } catch (error) { + console.error("Error fetching training courses:", error); + res.status(500).json({ message: "Failed to fetch training courses" }); + } + }); + + app.post("/api/training-courses", isAuthenticated, async (req, res) => { + try { + const course = await storage.createTrainingCourse(req.body); + res.json(course); + } catch (error) { + console.error("Error creating training course:", error); + res.status(500).json({ message: "Failed to create training course" }); + } + }); + + app.patch("/api/training-courses/:id", isAuthenticated, async (req, res) => { + try { + const updated = await storage.updateTrainingCourse(req.params.id, req.body); + if (!updated) { + return res.status(404).json({ message: "Training course not found" }); + } + res.json(updated); + } catch (error) { + console.error("Error updating training course:", error); + res.status(500).json({ message: "Failed to update training course" }); + } + }); + + app.delete("/api/training-courses/:id", isAuthenticated, async (req, res) => { + try { + await storage.deleteTrainingCourse(req.params.id); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting training course:", error); + res.status(500).json({ message: "Failed to delete training course" }); + } + }); + + // ============= HOLIDAYS ROUTES ============= + app.get("/api/holidays", isAuthenticated, async (req, res) => { + try { + const year = req.query.year ? parseInt(req.query.year as string) : undefined; + const holidays = await storage.getAllHolidays(year); + res.json(holidays); + } catch (error) { + console.error("Error fetching holidays:", error); + res.status(500).json({ message: "Failed to fetch holidays" }); + } + }); + + app.post("/api/holidays", isAuthenticated, async (req, res) => { + try { + const holiday = await storage.createHoliday(req.body); + res.json(holiday); + } catch (error) { + console.error("Error creating holiday:", error); + res.status(500).json({ message: "Failed to create holiday" }); + } + }); + + app.delete("/api/holidays/:id", isAuthenticated, async (req, res) => { + try { + await storage.deleteHoliday(req.params.id); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting holiday:", error); + res.status(500).json({ message: "Failed to delete holiday" }); + } + }); + + // ============= ABSENCES ROUTES ============= + app.get("/api/absences", isAuthenticated, async (req, res) => { + try { + const guardId = req.query.guardId as string | undefined; + const absences = guardId + ? await storage.getAbsencesByGuard(guardId) + : await storage.getAllAbsences(); + res.json(absences); + } catch (error) { + console.error("Error fetching absences:", error); + res.status(500).json({ message: "Failed to fetch absences" }); + } + }); + + app.post("/api/absences", isAuthenticated, async (req, res) => { + try { + const absence = await storage.createAbsence(req.body); + res.json(absence); + } catch (error) { + console.error("Error creating absence:", error); + res.status(500).json({ message: "Failed to create absence" }); + } + }); + + app.patch("/api/absences/:id", isAuthenticated, async (req, res) => { + try { + const updated = await storage.updateAbsence(req.params.id, req.body); + if (!updated) { + return res.status(404).json({ message: "Absence not found" }); + } + res.json(updated); + } catch (error) { + console.error("Error updating absence:", error); + res.status(500).json({ message: "Failed to update absence" }); + } + }); + + app.delete("/api/absences/:id", isAuthenticated, async (req, res) => { + try { + await storage.deleteAbsence(req.params.id); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting absence:", error); + res.status(500).json({ message: "Failed to delete absence" }); + } + }); + const httpServer = createServer(app); return httpServer; } diff --git a/server/storage.ts b/server/storage.ts index 7cfde7b..1d2edfa 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -7,6 +7,13 @@ import { shifts, shiftAssignments, notifications, + guardConstraints, + sitePreferences, + trainingCourses, + holidays, + holidayAssignments, + absences, + absenceAffectedShifts, type User, type UpsertUser, type Guard, @@ -21,6 +28,20 @@ import { 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, } from "@shared/schema"; import { db } from "./db"; import { eq, and, gte, lte, desc } from "drizzle-orm"; @@ -64,6 +85,44 @@ export interface IStorage { getNotificationsByUser(userId: string): Promise; createNotification(notification: InsertNotification): Promise; markNotificationAsRead(id: string): Promise; + + // Guard Constraints operations + getGuardConstraints(guardId: string): Promise; + upsertGuardConstraints(constraints: InsertGuardConstraints): Promise; + + // Site Preferences operations + getSitePreferences(siteId: string): Promise; + createSitePreference(pref: InsertSitePreference): Promise; + deleteSitePreference(id: string): Promise; + + // Training Courses operations + getTrainingCoursesByGuard(guardId: string): Promise; + getAllTrainingCourses(): Promise; + createTrainingCourse(course: InsertTrainingCourse): Promise; + updateTrainingCourse(id: string, course: Partial): Promise; + deleteTrainingCourse(id: string): Promise; + + // Holidays operations + getAllHolidays(year?: number): Promise; + createHoliday(holiday: InsertHoliday): Promise; + deleteHoliday(id: string): Promise; + + // Holiday Assignments operations + getHolidayAssignments(holidayId: string): Promise; + createHolidayAssignment(assignment: InsertHolidayAssignment): Promise; + deleteHolidayAssignment(id: string): Promise; + + // Absences operations + getAllAbsences(): Promise; + getAbsencesByGuard(guardId: string): Promise; + createAbsence(absence: InsertAbsence): Promise; + updateAbsence(id: string, absence: Partial): Promise; + deleteAbsence(id: string): Promise; + + // Absence Affected Shifts operations + getAffectedShiftsByAbsence(absenceId: string): Promise; + createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise; + deleteAbsenceAffectedShift(id: string): Promise; } export class DatabaseStorage implements IStorage { @@ -289,6 +348,168 @@ export class DatabaseStorage implements IStorage { .set({ isRead: true }) .where(eq(notifications.id, id)); } + + // Guard Constraints operations + async getGuardConstraints(guardId: string): Promise { + const [constraints] = await db + .select() + .from(guardConstraints) + .where(eq(guardConstraints.guardId, guardId)); + return constraints; + } + + async upsertGuardConstraints(constraintsData: InsertGuardConstraints): Promise { + 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 { + return await db + .select() + .from(sitePreferences) + .where(eq(sitePreferences.siteId, siteId)); + } + + async createSitePreference(pref: InsertSitePreference): Promise { + const [newPref] = await db.insert(sitePreferences).values(pref).returning(); + return newPref; + } + + async deleteSitePreference(id: string): Promise { + await db.delete(sitePreferences).where(eq(sitePreferences.id, id)); + } + + // Training Courses operations + async getTrainingCoursesByGuard(guardId: string): Promise { + return await db + .select() + .from(trainingCourses) + .where(eq(trainingCourses.guardId, guardId)) + .orderBy(desc(trainingCourses.scheduledDate)); + } + + async getAllTrainingCourses(): Promise { + return await db.select().from(trainingCourses).orderBy(desc(trainingCourses.scheduledDate)); + } + + async createTrainingCourse(course: InsertTrainingCourse): Promise { + const [newCourse] = await db.insert(trainingCourses).values(course).returning(); + return newCourse; + } + + async updateTrainingCourse(id: string, courseData: Partial): Promise { + const [updated] = await db + .update(trainingCourses) + .set(courseData) + .where(eq(trainingCourses.id, id)) + .returning(); + return updated; + } + + async deleteTrainingCourse(id: string): Promise { + await db.delete(trainingCourses).where(eq(trainingCourses.id, id)); + } + + // Holidays operations + async getAllHolidays(year?: number): Promise { + 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 { + const [newHoliday] = await db.insert(holidays).values(holiday).returning(); + return newHoliday; + } + + async deleteHoliday(id: string): Promise { + await db.delete(holidays).where(eq(holidays.id, id)); + } + + // Holiday Assignments operations + async getHolidayAssignments(holidayId: string): Promise { + return await db + .select() + .from(holidayAssignments) + .where(eq(holidayAssignments.holidayId, holidayId)); + } + + async createHolidayAssignment(assignment: InsertHolidayAssignment): Promise { + const [newAssignment] = await db.insert(holidayAssignments).values(assignment).returning(); + return newAssignment; + } + + async deleteHolidayAssignment(id: string): Promise { + await db.delete(holidayAssignments).where(eq(holidayAssignments.id, id)); + } + + // Absences operations + async getAllAbsences(): Promise { + return await db.select().from(absences).orderBy(desc(absences.startDate)); + } + + async getAbsencesByGuard(guardId: string): Promise { + return await db + .select() + .from(absences) + .where(eq(absences.guardId, guardId)) + .orderBy(desc(absences.startDate)); + } + + async createAbsence(absence: InsertAbsence): Promise { + const [newAbsence] = await db.insert(absences).values(absence).returning(); + return newAbsence; + } + + async updateAbsence(id: string, absenceData: Partial): Promise { + const [updated] = await db + .update(absences) + .set(absenceData) + .where(eq(absences.id, id)) + .returning(); + return updated; + } + + async deleteAbsence(id: string): Promise { + await db.delete(absences).where(eq(absences.id, id)); + } + + // Absence Affected Shifts operations + async getAffectedShiftsByAbsence(absenceId: string): Promise { + return await db + .select() + .from(absenceAffectedShifts) + .where(eq(absenceAffectedShifts.absenceId, absenceId)); + } + + async createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise { + const [newAffected] = await db.insert(absenceAffectedShifts).values(affected).returning(); + return newAffected; + } + + async deleteAbsenceAffectedShift(id: string): Promise { + await db.delete(absenceAffectedShifts).where(eq(absenceAffectedShifts.id, id)); + } } export const storage = new DatabaseStorage();