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 (
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+
+ {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();