commit e4f63a135ee6222357d4a0d3460b673c43bae2e0 Author: pettrop Date: Tue Feb 3 08:53:22 2026 +0100 Initial commit: Helpdesk application setup - Backend: Node.js/TypeScript with Prisma ORM - Frontend: Vite + TypeScript - Project configuration and documentation Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a365df --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +build/ +.output/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ + +# TypeScript +*.tsbuildinfo + +# Temporary files +tmp/ +temp/ +.tmp/ + +# Uploads (keep structure, ignore content) +uploads/* +!uploads/.gitkeep + +# Database +*.sqlite +*.db + +# Prisma +prisma/*.db +prisma/*.db-journal diff --git a/HELPDESK_INIT_V2.md b/HELPDESK_INIT_V2.md new file mode 100644 index 0000000..ef34470 --- /dev/null +++ b/HELPDESK_INIT_V2.md @@ -0,0 +1,2533 @@ +# 🎯 Helpdesk/Task Manager System V2 - Inicializačná Špecifikácia + +## 📊 Prehľad Projektu + +**Názov:** Helpdesk & Task Management System (Extended) +**Verzia:** 2.0.0 +**Účel:** Komplexný systém pre správu zákaziek, úloh, projektov, revíznych kontrol a reklamácií s dôrazom na flexibilitu a konfigurovateľnosť +**Jazyk UI:** Slovenčina +**Dátum:** 02.02.2026 + +--- + +## 🆕 Novinky vo V2 + +### **Hlavné Rozšírenia:** + +1. **🔧 Equipment Management** - Evidencia zariadení s revíznymi kontrolami +2. **📝 RMA System** - Kompletný reklamačný proces +3. **👥 Customer Database** - Centralizovaná zákaznícka databáza +4. **⚙️ Dynamic Configuration** - Všetky typy, stavy a nastavenia konfigurovateľné cez GUI +5. **🔄 Workflow Engine** - Flexibilné workflow pravidlá +6. **🏠 Self-Hosted First** - Kompletne zadarmo, bez platených služieb + +### **Odstránené:** +- ❌ Pevné ENUMs v databáze +- ❌ Hardcoded business logika +- ❌ Platené závislosti + +### **Pridané:** +- ✅ Configuration-driven architecture +- ✅ ROOT Settings panel +- ✅ External DB import pre zákazníkov +- ✅ Dynamic workflow rules +- ✅ Multi-entity tagging system + +--- + +## 🎨 Primárny UI Koncept + +### SWIMLANES BY PROJECT (Hlavný View - Nezmenené) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Filtre: [👤 Moje] [📅 Dnes+Zajtra] [🔴 Vysoká priorita] │ +│ Zobrazenie: [🏊 Swimlanes] [📋 List] [📅 Timeline] │ +└─────────────────────────────────────────────────────────┘ + +╔═══════════════════════════════════════════════════════════╗ +║ 📊 PROJEKT: Renovácia kancelárie XY [▼] ║ +║ Zákazník: Firma ABC | Deadline: 15.02.2026 | Zodp: Martin║ +╠════════════╦═════════════╦═════════════╦═════════════════╣ +║ 🆕 NEW (2) ║ 🔄 DOING(3) ║ ✅ DONE (1) ║ 📊 STATS ║ +╠════════════╬═════════════╬═════════════╬═════════════════╣ +║ [Task 1]🔴 ║ [Task 3]🟡 ║ [Task 6] ║ Progress: 60% ║ +║ [Task 2]🟡 ║ [Task 4]🟡 ║ ║ 3 dni do DDL ║ +║ ║ [Task 5]🟢 ║ ║ 🏷️ #ponuka ║ +║ + Nový task║ ║ ║ [⚙️] [📈] [💬] ║ +╚════════════╩═════════════╩═════════════╩═════════════════╝ +``` + +--- + +## 🏗️ Technická Architektúra + +### Tech Stack (Nezmenené) + +```yaml +Backend: + - Runtime: Node.js (v20+) + - Framework: Express.js + - Database: PostgreSQL (v16+) + - ORM: Prisma + - Auth: JWT + bcrypt + - Validation: Zod + - Testing: Jest + Supertest + +Frontend: + - Framework: React 18+ (TypeScript) + - State: Zustand + - Routing: React Router v6 + - UI Library: shadcn/ui + Tailwind CSS + - Drag & Drop: dnd-kit + - Forms: React Hook Form + Zod + - Notifications: react-hot-toast + - Testing: Vitest + React Testing Library + +DevOps (Self-Hosted): + - Containerization: Docker + Docker Compose + - Reverse Proxy: Nginx + - SSL: Let's Encrypt (free) + - Monitoring: Prometheus + Grafana (self-hosted) + - Logging: Loki + Promtail (self-hosted) + - CI/CD: GitHub Actions (free tier) + - Email: Postfix (self-hosted) alebo Gmail SMTP (free) + - Backups: Cron + rsync (self-hosted) +``` + +--- + +## 📐 Databázová Schéma (Prisma) - KOMPLETNÁ V2 + +### 🎯 Architektúra Princípy + +``` +1. DYNAMICKÉ TYPY - Žiadne ENUMs, všetko v tabuľkách +2. KONFIGUROVATEĽNOSŤ - ROOT môže meniť všetko cez GUI +3. AUDIT TRAIL - Každá zmena je zalogovaná +4. SOFT DELETE - Záznamy sa označujú ako neaktívne, nemažú sa +5. SELF-REFERENCING - Hierarchie (parent-child) +``` + +--- + +### Kompletný Prisma Schema + +```prisma +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ==================== USER ROLES (dynamické) ==================== + +model UserRole { + id String @id @default(cuid()) + code String @unique // "ROOT", "ADMIN", "USER", "CUSTOMER" + name String // "Root správca" + description String? + + // Oprávnenia (JSON) + permissions Json // { "projects": ["create", "read", "update", "delete", "all"], ... } + + level Int // Hierarchia: 1=ROOT, 2=ADMIN, 3=USER, 4=CUSTOMER + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + users User[] + + @@index([active]) + @@index([level]) +} + +// ==================== USERS ==================== + +model User { + id String @id @default(cuid()) + email String @unique + password String // bcrypt hashed + name String + + // Role relation (namiesto enum) + roleId String + role UserRole @relation(fields: [roleId], references: [id]) + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + ownedProjects Project[] @relation("ProjectOwner") + assignedProjects ProjectMember[] + createdTasks Task[] @relation("TaskCreator") + assignedTasks TaskAssignee[] + reminders Reminder[] + activityLogs ActivityLog[] + + // Equipment + createdEquipment Equipment[] @relation("EquipmentCreator") + performedRevisions Revision[] + uploadedEquipmentFiles EquipmentAttachment[] + + // RMA + assignedRMAs RMA[] @relation("RMAAssignee") + createdRMAs RMA[] @relation("RMACreator") + approvedRMAs RMA[] @relation("RMAApprover") + rmaAttachments RMAAttachment[] + rmaStatusChanges RMAStatusHistory[] + rmaComments RMAComment[] + + // Customers + createdCustomers Customer[] + + @@index([email]) + @@index([roleId]) + @@index([active]) +} + +// ==================== CONFIGURATION TABLES ==================== + +// Equipment Types (dynamické, namiesto enum) +model EquipmentType { + id String @id @default(cuid()) + code String @unique // "EPS", "HSP", "CAMERA" + name String // "Elektrická požiarna signalizácia" + description String? + color String? // Hex farba (#FF5733) + icon String? // Lucide icon name + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + equipment Equipment[] + + @@index([active]) + @@index([order]) +} + +// Revision Types (dynamické, namiesto enum) +model RevisionType { + id String @id @default(cuid()) + code String @unique // "QUARTERLY", "ANNUAL" + name String // "Štvrťročná revízia" + intervalDays Int // Interval (90, 365...) + reminderDays Int @default(14) // Pripomenúť X dní dopredu + color String? + description String? + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + revisions Revision[] + + @@index([active]) + @@index([order]) +} + +// RMA Statuses (dynamické, namiesto enum) +model RMAStatus { + id String @id @default(cuid()) + code String @unique // "NEW", "IN_ASSESSMENT" + name String // "Nová reklamácia" + description String? + color String? + icon String? + order Int @default(0) + + // Workflow + isInitial Boolean @default(false) // Štartovací stav + isFinal Boolean @default(false) // Konečný stav + canTransitionTo Json? // Array: ["IN_ASSESSMENT", "REJECTED"] + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rmas RMA[] + + @@index([active]) + @@index([order]) + @@index([isInitial]) + @@index([isFinal]) +} + +// RMA Solutions (dynamické, namiesto enum) +model RMASolution { + id String @id @default(cuid()) + code String @unique // "REPAIR", "REPLACEMENT" + name String // "Oprava" + description String? + color String? + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rmas RMA[] + + @@index([active]) + @@index([order]) +} + +// Task Statuses (dynamické, namiesto enum) +model TaskStatus { + id String @id @default(cuid()) + code String @unique // "NEW", "IN_PROGRESS", "DONE" + name String // "Nová úloha" + description String? + color String? + icon String? + order Int @default(0) + + // Swimlane mapping + swimlaneColumn String? // "NEW", "DOING", "DONE" + + isInitial Boolean @default(false) + isFinal Boolean @default(false) + canTransitionTo Json? + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tasks Task[] + + @@index([active]) + @@index([swimlaneColumn]) + @@index([order]) +} + +// Priorities (dynamické, namiesto enum) +model Priority { + id String @id @default(cuid()) + code String @unique // "LOW", "MEDIUM", "HIGH" + name String // "Vysoká priorita" + description String? + color String? + icon String? + level Int // 1=lowest, 10=highest + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tasks Task[] + + @@index([active]) + @@index([level]) + @@index([order]) +} + +// Tags (univerzálne pre všetky entity) +model Tag { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + + entityType String // "PROJECT", "TASK", "EQUIPMENT", "RMA" + + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + projectTags ProjectTag[] + taskTags TaskTag[] + equipmentTags EquipmentTag[] + rmaTags RMATag[] + + @@index([entityType]) + @@index([active]) +} + +// System Settings (key-value configuration) +model SystemSetting { + id String @id @default(cuid()) + + key String @unique // "REVISION_REMINDER_DAYS" + value Json // Flexible value (string, number, boolean, object) + + category String // "NOTIFICATIONS", "RMA", "EQUIPMENT" + label String // Human-readable + description String? + dataType String // "string", "number", "boolean", "json" + + validation Json? // { min: 1, max: 365, required: true } + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([category]) +} + +// ==================== CUSTOMERS ==================== + +model Customer { + id String @id @default(cuid()) + + // Základné údaje + name String // Názov firmy alebo meno + address String? + email String? + phone String? + ico String? // IČO + dic String? // DIČ + icdph String? // IČ DPH + + // Kontaktná osoba + contactPerson String? + contactEmail String? + contactPhone String? + + // Import z externej DB + externalId String? @unique // ID z externej databázy + externalSource String? // Názov zdroja ("SAP", "Legacy") + + notes String? @db.Text + active Boolean @default(true) + + createdById String + createdBy User @relation(fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + projects Project[] + equipment Equipment[] + rmas RMA[] + + @@index([name]) + @@index([ico]) + @@index([externalId]) + @@index([active]) +} + +// ==================== PROJECTS ==================== + +model Project { + id String @id @default(cuid()) + name String + description String? + + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + ownerId String + owner User @relation("ProjectOwner", fields: [ownerId], references: [id]) + + // Status relation (namiesto enum) + statusId String + status TaskStatus @relation(fields: [statusId], references: [id]) + + softDeadline DateTime? // Makký deadline (warning) + hardDeadline DateTime? // Finálny deadline (critical) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + tasks Task[] + members ProjectMember[] + tags ProjectTag[] + + @@index([ownerId]) + @@index([statusId]) + @@index([customerId]) + @@index([hardDeadline]) +} + +model ProjectMember { + id String @id @default(cuid()) + projectId String + userId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + addedAt DateTime @default(now()) + + @@unique([projectId, userId]) + @@index([projectId]) + @@index([userId]) +} + +model ProjectTag { + projectId String + tagId String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([projectId, tagId]) +} + +// ==================== TASKS ==================== + +model Task { + id String @id @default(cuid()) + title String + description String? + + projectId String? + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) + + parentId String? + parent Task? @relation("SubTasks", fields: [parentId], references: [id], onDelete: Cascade) + subTasks Task[] @relation("SubTasks") + + // Status relation (namiesto enum) + statusId String + status TaskStatus @relation(fields: [statusId], references: [id]) + + // Priority relation (namiesto enum) + priorityId String + priority Priority @relation(fields: [priorityId], references: [id]) + + deadline DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + createdById String + createdBy User @relation("TaskCreator", fields: [createdById], references: [id]) + + assignees TaskAssignee[] + reminders Reminder[] + comments Comment[] + tags TaskTag[] + + @@index([projectId]) + @@index([parentId]) + @@index([statusId]) + @@index([priorityId]) + @@index([deadline]) + @@index([createdById]) +} + +model TaskAssignee { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + assignedAt DateTime @default(now()) + + @@unique([taskId, userId]) + @@index([taskId]) + @@index([userId]) +} + +model TaskTag { + taskId String + tagId String + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([taskId, tagId]) +} + +model Reminder { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + remindAt DateTime + snoozedUntil DateTime? + dismissed Boolean @default(false) + + message String? + + createdAt DateTime @default(now()) + + @@index([userId, remindAt]) + @@index([taskId]) +} + +model Comment { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + + content String @db.Text + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId]) + @@index([createdAt]) +} + +// ==================== EQUIPMENT MANAGEMENT ==================== + +model Equipment { + id String @id @default(cuid()) + + name String + + // Type relation (namiesto enum) + typeId String + type EquipmentType @relation(fields: [typeId], references: [id]) + + brand String? + model String? + + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + address String + location String? // Presné umiestnenie + + partNumber String? // PN + serialNumber String? // SN + + installDate DateTime? + warrantyEnd DateTime? + warrantyStatus String? // "ACTIVE", "EXPIRED", "EXTENDED" + + description String? @db.Text + notes String? @db.Text + + active Boolean @default(true) + + createdById String + createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + revisions Revision[] + attachments EquipmentAttachment[] + tags EquipmentTag[] + + @@index([typeId]) + @@index([customerId]) + @@index([warrantyEnd]) + @@index([active]) + @@index([createdById]) +} + +model Revision { + id String @id @default(cuid()) + + equipmentId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + + // Type relation (namiesto enum) + typeId String + type RevisionType @relation(fields: [typeId], references: [id]) + + performedDate DateTime + nextDueDate DateTime? // Auto-calculated: performedDate + type.intervalDays + + performedById String + performedBy User @relation(fields: [performedById], references: [id]) + + findings String? @db.Text + result String? // "OK", "MINOR_ISSUES", "CRITICAL" + notes String? @db.Text + + reminderSent Boolean @default(false) + reminderDate DateTime? // Auto-calculated: nextDueDate - type.reminderDays + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([equipmentId]) + @@index([typeId]) + @@index([performedById]) + @@index([nextDueDate]) + @@index([reminderDate]) +} + +model EquipmentAttachment { + id String @id @default(cuid()) + equipmentId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + + filename String + filepath String + mimetype String + size Int + + uploadedById String + uploadedBy User @relation(fields: [uploadedById], references: [id]) + + uploadedAt DateTime @default(now()) + + @@index([equipmentId]) +} + +model EquipmentTag { + equipmentId String + tagId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([equipmentId, tagId]) +} + +// ==================== RMA (REKLAMÁCIE) ==================== + +model RMA { + id String @id @default(cuid()) + + // Auto-generated RMA number + rmaNumber String @unique // Format: RMA-YYYYMMDDXXX + + // ===== ZÁKAZNÍK ===== + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + // Manual entry (ak nie je v DB) + customerName String? + customerAddress String? + customerEmail String? + customerPhone String? + customerICO String? + submittedBy String // Meno osoby + + // ===== VÝROBOK ===== + productName String + invoiceNumber String? + purchaseDate DateTime? + productNumber String? + serialNumber String? + accessories String? @db.Text + + // ===== REKLAMÁCIA ===== + issueDescription String @db.Text + + // Status relation (namiesto enum) + statusId String + status RMAStatus @relation(fields: [statusId], references: [id]) + + // Solution relation (namiesto enum) + proposedSolutionId String? + proposedSolution RMASolution? @relation(fields: [proposedSolutionId], references: [id]) + + // ===== WORKFLOW ===== + requiresApproval Boolean @default(false) // Dynamicky z workflow rules + approvedById String? + approvedBy User? @relation("RMAApprover", fields: [approvedById], references: [id]) + approvedAt DateTime? + + // ===== SPRACOVANIE ===== + receivedDate DateTime? + receivedLocation String? + internalNotes String? @db.Text + + resolutionDate DateTime? + resolutionNotes String? @db.Text + + assignedToId String? + assignedTo User? @relation("RMAAssignee", fields: [assignedToId], references: [id]) + + createdById String + createdBy User @relation("RMACreator", fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + closedAt DateTime? + + attachments RMAAttachment[] + statusHistory RMAStatusHistory[] + comments RMAComment[] + tags RMATag[] + + @@index([rmaNumber]) + @@index([customerId]) + @@index([statusId]) + @@index([proposedSolutionId]) + @@index([assignedToId]) + @@index([createdById]) + @@index([purchaseDate]) + @@index([receivedDate]) +} + +model RMAAttachment { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + filename String + filepath String + mimetype String + size Int + + uploadedById String + uploadedBy User @relation(fields: [uploadedById], references: [id]) + + uploadedAt DateTime @default(now()) + + @@index([rmaId]) +} + +model RMAStatusHistory { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + fromStatusId String? + toStatusId String + + changedById String + changedBy User @relation(fields: [changedById], references: [id]) + + notes String? @db.Text + changedAt DateTime @default(now()) + + @@index([rmaId]) + @@index([changedAt]) +} + +model RMAComment { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + content String @db.Text + + userId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([rmaId]) + @@index([createdAt]) +} + +model RMATag { + rmaId String + tagId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([rmaId, tagId]) +} + +// ==================== ACTIVITY LOG ==================== + +model ActivityLog { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + action String // "CREATE", "UPDATE", "DELETE", "STATUS_CHANGE" + entity String // "Project", "Task", "RMA", "Equipment" + entityId String + + changes Json? // Snapshot of changes + ipAddress String? + userAgent String? + + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([entity, entityId]) + @@index([createdAt]) +} +``` + +--- + +## 🔐 Bezpečnosť & Autentifikácia (Nezmenené) + +### JWT Auth Flow + +``` +1. Login: + POST /api/auth/login + → Validate credentials + → Generate accessToken (15min) + → Generate refreshToken (7 days) + +2. Protected Routes: + Header: Authorization: Bearer + → Middleware verifies JWT + → Middleware checks user.role permissions + +3. Refresh: + POST /api/auth/refresh + +4. Logout: + POST /api/auth/logout +``` + +### RBAC (Dynamic from Database) + +```typescript +// Permissions načítané z UserRole tabuľky +const userRole = await prisma.userRole.findUnique({ + where: { id: user.roleId }, +}); + +const permissions = userRole.permissions; // JSON object + +// Middleware check +if (!hasPermission(permissions, 'projects', 'create')) { + return res.status(403).json({ error: 'Forbidden' }); +} +``` + +--- + +## 🎯 API Endpoints - KOMPLETNÉ + +### Authentication (Nezmenené) + +``` +POST /api/auth/register +POST /api/auth/login +POST /api/auth/refresh +POST /api/auth/logout +GET /api/auth/me +``` + +### Users + +``` +GET /api/users // Stránkovaný zoznam (admin only) +GET /api/users/simple // Jednoduchý zoznam pre selecty (server-side search: ?search=meno) +GET /api/users/:id +PUT /api/users/:id +DELETE /api/users/:id +PATCH /api/users/:id/role +``` + +### Projects + +``` +GET /api/projects +POST /api/projects +GET /api/projects/:id +PUT /api/projects/:id +DELETE /api/projects/:id +PATCH /api/projects/:id/status +GET /api/projects/:id/tasks +POST /api/projects/:id/members +DELETE /api/projects/:id/members/:userId +``` + +### Tasks + +``` +GET /api/tasks +POST /api/tasks +GET /api/tasks/:id +PUT /api/tasks/:id +DELETE /api/tasks/:id +PATCH /api/tasks/:id/status +POST /api/tasks/:id/assignees +DELETE /api/tasks/:id/assignees/:userId +GET /api/tasks/:id/comments +POST /api/tasks/:id/comments +``` + +### **🆕 Customers** + +``` +GET /api/customers +POST /api/customers +GET /api/customers/:id +PUT /api/customers/:id +DELETE /api/customers/:id +GET /api/customers/:id/projects +GET /api/customers/:id/equipment +GET /api/customers/:id/rmas +POST /api/customers/import // Import z externej DB +``` + +### **🆕 Equipment** + +``` +GET /api/equipment +POST /api/equipment +GET /api/equipment/:id +PUT /api/equipment/:id +DELETE /api/equipment/:id +GET /api/equipment/:id/revisions +POST /api/equipment/:id/revisions +POST /api/equipment/:id/attachments +GET /api/equipment/reminders // Upcoming revisions +``` + +### **🆕 Revisions** + +``` +GET /api/revisions +GET /api/revisions/upcoming +GET /api/revisions/overdue +PUT /api/revisions/:id +DELETE /api/revisions/:id +``` + +### **🆕 RMA** + +``` +GET /api/rma +POST /api/rma +GET /api/rma/:id +PUT /api/rma/:id +DELETE /api/rma/:id +PATCH /api/rma/:id/status +PATCH /api/rma/:id/approve // Admin approval +POST /api/rma/:id/attachments +POST /api/rma/:id/comments +GET /api/rma/:id/pdf // Generate PDF +GET /api/rma/generate-number // Next RMA number +``` + +### **🆕 Settings (ROOT only)** + +``` +// Equipment Types +GET /api/settings/equipment-types +POST /api/settings/equipment-types +PUT /api/settings/equipment-types/:id +DELETE /api/settings/equipment-types/:id +PATCH /api/settings/equipment-types/:id/reorder + +// Revision Types +GET /api/settings/revision-types +POST /api/settings/revision-types +PUT /api/settings/revision-types/:id +DELETE /api/settings/revision-types/:id + +// RMA Statuses +GET /api/settings/rma-statuses +POST /api/settings/rma-statuses +PUT /api/settings/rma-statuses/:id +DELETE /api/settings/rma-statuses/:id +GET /api/settings/rma-statuses/:id/transitions + +// RMA Solutions +GET /api/settings/rma-solutions +POST /api/settings/rma-solutions +PUT /api/settings/rma-solutions/:id +DELETE /api/settings/rma-solutions/:id + +// Task Statuses +GET /api/settings/task-statuses +POST /api/settings/task-statuses +PUT /api/settings/task-statuses/:id +DELETE /api/settings/task-statuses/:id + +// Priorities +GET /api/settings/priorities +POST /api/settings/priorities +PUT /api/settings/priorities/:id +DELETE /api/settings/priorities/:id + +// Tags +GET /api/settings/tags?entityType=PROJECT +POST /api/settings/tags +PUT /api/settings/tags/:id +DELETE /api/settings/tags/:id + +// System Settings +GET /api/settings/system +GET /api/settings/system/:key +PUT /api/settings/system/:key +GET /api/settings/system/category/:category + +// User Roles +GET /api/settings/roles +POST /api/settings/roles +PUT /api/settings/roles/:id +DELETE /api/settings/roles/:id +``` + +### Dashboard + +``` +GET /api/dashboard // Hlavné štatistiky (projects, tasks, customers, equipment, rma) +GET /api/dashboard/today // Moje úlohy + moje projekty +GET /api/dashboard/week // Úlohy s termínom tento týždeň +GET /api/dashboard/stats // Detailné štatistiky +GET /api/dashboard/reminders // Tasks + Equipment revisions +``` + +### Activity Logs (ROOT only) + +``` +GET /api/logs +GET /api/logs/:entityType/:entityId +``` + +--- + +## 🎨 Frontend Komponenty + +### Rozšírená Štruktúra + +``` +src/ +├── components/ +│ ├── auth/ +│ │ ├── LoginForm.tsx +│ │ ├── ProtectedRoute.tsx +│ │ └── RoleGuard.tsx +│ │ +│ ├── layout/ +│ │ ├── Header.tsx +│ │ ├── Sidebar.tsx +│ │ └── MainLayout.tsx +│ │ +│ ├── dashboard/ +│ │ ├── DashboardView.tsx +│ │ ├── TodaysTasks.tsx +│ │ ├── RemindersWidget.tsx +│ │ ├── EquipmentReminders.tsx # NEW +│ │ ├── RMAWidget.tsx # NEW +│ │ └── StatsCard.tsx +│ │ +│ ├── swimlanes/ +│ │ ├── SwimlanesBoard.tsx +│ │ ├── ProjectSwimlane.tsx +│ │ ├── TaskCard.tsx +│ │ ├── ColumnContainer.tsx +│ │ └── QuickActions.tsx +│ │ +│ ├── tasks/ +│ │ ├── TaskList.tsx +│ │ ├── TaskDetail.tsx +│ │ ├── TaskForm.tsx +│ │ ├── InlineCommentEditor.tsx +│ │ ├── AssigneeSelector.tsx +│ │ └── PriorityBadge.tsx +│ │ +│ ├── projects/ +│ │ ├── ProjectList.tsx +│ │ ├── ProjectDetail.tsx +│ │ ├── ProjectForm.tsx +│ │ └── ProjectProgress.tsx +│ │ +│ ├── customers/ # NEW +│ │ ├── CustomerList.tsx +│ │ ├── CustomerDetail.tsx +│ │ ├── CustomerForm.tsx +│ │ └── CustomerImport.tsx +│ │ +│ ├── equipment/ # NEW +│ │ ├── EquipmentList.tsx +│ │ ├── EquipmentDetail.tsx +│ │ ├── EquipmentForm.tsx +│ │ ├── RevisionForm.tsx +│ │ ├── RevisionCalendar.tsx +│ │ └── EquipmentCard.tsx +│ │ +│ ├── rma/ # NEW +│ │ ├── RMAList.tsx +│ │ ├── RMADetail.tsx +│ │ ├── RMAForm.tsx +│ │ ├── RMAStatusBadge.tsx +│ │ ├── RMAWorkflow.tsx +│ │ └── RMAPDFExport.tsx +│ │ +│ ├── settings/ # NEW +│ │ ├── SettingsDashboard.tsx +│ │ ├── EquipmentTypesSettings.tsx +│ │ ├── RevisionTypesSettings.tsx +│ │ ├── RMAStatusSettings.tsx +│ │ ├── RMASolutionSettings.tsx +│ │ ├── TaskStatusSettings.tsx +│ │ ├── PrioritySettings.tsx +│ │ ├── TagSettings.tsx +│ │ ├── SystemSettings.tsx +│ │ └── RoleSettings.tsx +│ │ +│ ├── shared/ +│ │ ├── Button.tsx +│ │ ├── Modal.tsx +│ │ ├── Dropdown.tsx +│ │ ├── DatePicker.tsx +│ │ ├── SearchBar.tsx +│ │ ├── FileUpload.tsx # NEW +│ │ ├── ColorPicker.tsx # NEW +│ │ ├── IconPicker.tsx # NEW +│ │ └── Toast.tsx +│ │ +│ └── ui/ (shadcn/ui) +``` + +--- + +## 🚀 Development Fázy (Aktualizované) + +### **FÁZA 1: MVP + Configuration Foundation** ✅ DOKONČENÁ + +**Cieľ:** Základná funkcionalita + dynamická konfigurácia + +**Backend:** +- [x] Setup projektu (Express, Prisma, PostgreSQL) +- [x] **Databázová schéma V2** (všetky configuration tables) +- [x] JWT Auth (access + refresh tokens) +- [x] RBAC middleware (dynamic permissions) +- [x] User CRUD + `/users/simple` endpoint (server-side search) +- [x] **User Roles CRUD** (dynamické role) +- [x] Project CRUD (s fallback pre default status) +- [x] Task CRUD (s fallback pre default status/priority) +- [x] Task Comments (s kontrolou oprávnení - len autor/priradený) +- [x] **Customer CRUD** +- [x] **Equipment CRUD** (bez revízií zatiaľ) +- [x] **RMA CRUD** (basic, bez workflow) +- [x] **Settings API** (CRUD pre všetky config tables) +- [x] **Dashboard API** (`/dashboard`, `/dashboard/today`, `/dashboard/stats`) +- [x] Activity logging +- [x] Seed data (default config) + +**Frontend:** +- [x] Setup (React 18, Vite, TypeScript, Tailwind CSS v4) +- [x] Auth pages (login s redirect po prihlásení) +- [x] Layout (header, sidebar, MainLayout) +- [x] **Dashboard** (štatistické karty + moje úlohy + moje projekty) +- [x] Project list/create/edit (s error handling) +- [x] Task list/create/edit (so stĺpcom "Zadal") +- [x] **TaskDetail** - detail úlohy s komentármi + - Kontrola oprávnení (komentovať môže len autor alebo priradený) + - Zobrazenie info o úlohe (zadal, priradení, projekt, termín) +- [x] **UserSelect** - server-side vyhľadávanie používateľov (debounce 300ms) +- [x] **Customer list/create/edit** +- [x] **Equipment list/create (basic form)** +- [x] **RMA list/create (basic form)** +- [x] **Settings Dashboard** (ROOT only) + - [x] Equipment Types + - [x] Revision Types + - [x] RMA Statuses/Solutions + - [x] Task Statuses + - [x] Priorities + - [x] User Roles +- [x] Basic forms (React Hook Form + Zod) s lepším error handling +- [x] Toast notifications (react-hot-toast) +- [x] React Query pre data fetching + +**Deliverable:** +``` +✅ Prihlásenie/odhlásenie (JWT + refresh tokens) +✅ Dynamické role a oprávnenia +✅ Projekty a úlohy (vytvorenie, editácia, mazanie) +✅ Detail úlohy s komentármi a zmenou statusu/priority +✅ Priradenie používateľov na úlohy (multi-select) +✅ Dashboard s mojimi úlohami a projektmi + urgentné úlohy +✅ Zákazníci +✅ Zariadenia (bez revízií) +✅ RMA (bez workflow) +✅ ROOT môže konfigurovať všetko cez Settings +✅ Žiadne hardcoded ENUMs +✅ Server-side vyhľadávanie používateľov (škálovateľné) +``` + +**Prihlasovacie údaje:** +- Root: `root@helpdesk.sk` / `root123` +- Admin: `admin@helpdesk.sk` / `admin123` +- User: `user@helpdesk.sk` / `user123` + +**Spustenie:** +```bash +# Backend (terminal 1) +cd backend && npm run dev + +# Frontend (terminal 2) +cd frontend && npm run dev + +# Seed databázy (ak treba) +cd backend && npx prisma db seed +``` + +**Čas:** 3-4 týždne +**Náklady:** €5-10/mesiac (VPS) + +--- + +### **FÁZA 2: Core Features + Workflow** 🔥 (4-5 týždňov) + +**Cieľ:** Swimlanes, revízie, RMA workflow, reminders + +**Backend:** +- [ ] **Revision system** + - [ ] Create revision endpoint + - [ ] Auto-calculate nextDueDate + - [ ] Reminder scheduler +- [ ] **RMA workflow** + - [ ] Status transitions validation + - [ ] Approval flow (customer RMAs) + - [ ] Auto-assign logic +- [ ] Task hierarchy (parent-child) +- [ ] Bulk operations +- [ ] Dashboard aggregations +- [ ] Email service (Postfix self-hosted) +- [ ] WebSocket (Socket.IO) +- [ ] File upload handling +- [ ] **Task notifications** (databázové - viditeľné na všetkých zariadeniach) + +**Frontend:** +- [ ] **Swimlanes Board** (dnd-kit) + - [ ] Project swimlanes + - [ ] Drag & Drop + - [ ] Collapse/expand + - [ ] Progress indicators +- [ ] **Equipment Management** + - [ ] Revision form + - [ ] Revision calendar view + - [ ] Reminder notifications +- [ ] **RMA Workflow** + - [ ] Status change UI + - [ ] Approval buttons (admin) + - [ ] File attachments + - [ ] Comments + - [ ] PDF export +- [ ] **Inline Quick Actions** +- [ ] **Quick Search (Ctrl+K)** +- [ ] Reminder management UI +- [ ] Filters & tags +- [ ] Real-time updates (WebSocket) +- [ ] **Notifikácie o nových komentároch/zmenách** (všetky zariadenia) + +**Deliverable:** +``` +✅ Všetko z Fázy 1 + +✅ Swimlanes board +✅ Revízny systém funguje +✅ RMA workflow s approval +✅ Email notifikácie +✅ Live updates (WebSocket) +✅ File uploads +✅ Task notifikácie (databázové, všetky zariadenia) +``` + +**Čas:** 4-5 týždňov +**Náklady:** €15-25/mesiac + +--- + +### **FÁZA 3: Advanced Features** 🚀 (4-6 týždňov) + +**Cieľ:** Import, export, analytics, advanced UI + +**Backend:** +- [ ] **External DB connector** (import zákazníkov) +- [ ] Advanced filtering & search +- [ ] Export dát (PDF, Excel) +- [ ] Template system +- [ ] Performance optimizations +- [ ] Caching (Redis) +- [ ] Rate limiting + +**Frontend:** +- [ ] **Customer import UI** +- [ ] Timeline/Calendar view +- [ ] Gantt-style visualization +- [ ] Bulk actions UI +- [ ] Task templates +- [ ] Statistics & reports dashboard +- [ ] Advanced keyboard shortcuts +- [ ] Activity stream sidebar +- [ ] Role-based dashboards +- [ ] Mobile responsive + +**Testing:** +- [ ] Performance testing +- [ ] Security audit +- [ ] Full E2E coverage + +**Deliverable:** +``` +✅ Production-ready systém +✅ Import zákazníkov z externých DB +✅ Komplexné reporty +✅ Optimalizovaný performance +✅ Full monitoring stack +``` + +**Čas:** 4-6 týždňov +**Náklady:** €20-40/mesiac + +--- + +## 🔧 Environment Variables + +### Backend (.env) + +```bash +# Database +DATABASE_URL="postgresql://user:password@localhost:5432/helpdesk_db" + +# JWT +JWT_SECRET="your-super-secret-key" +JWT_REFRESH_SECRET="your-refresh-secret" +JWT_EXPIRES_IN="15m" +JWT_REFRESH_EXPIRES_IN="7d" + +# Server +PORT=3001 +NODE_ENV="development" + +# Email (Fáza 2) +SMTP_HOST="localhost" # Postfix self-hosted +SMTP_PORT=25 +SMTP_USER="" +SMTP_PASSWORD="" +EMAIL_FROM="noreply@vasadomena.sk" + +# Frontend URL +FRONTEND_URL="http://localhost:5173" + +# File Upload +UPLOAD_DIR="/app/uploads" +MAX_FILE_SIZE=10485760 # 10MB + +# Redis (Fáza 3) +REDIS_URL="redis://localhost:6379" + +# External DB (Fáza 3) +EXTERNAL_DB_TYPE="mysql" # mysql, mssql, oracle +EXTERNAL_DB_HOST="external-server.com" +EXTERNAL_DB_PORT=3306 +EXTERNAL_DB_USER="readonly" +EXTERNAL_DB_PASSWORD="password" +EXTERNAL_DB_NAME="customers" +``` + +--- + +## 📋 Seed Data (Default Configuration) + +```typescript +// prisma/seed.ts + +async function seed() { + console.log('🌱 Seeding database...'); + + // ===== USER ROLES ===== + const roles = await Promise.all([ + prisma.userRole.create({ + data: { + code: 'ROOT', + name: 'Root Správca', + level: 1, + permissions: { + projects: ['*'], + tasks: ['*'], + equipment: ['*'], + rma: ['*'], + customers: ['*'], + settings: ['*'], + users: ['*'], + logs: ['*'], + }, + }, + }), + prisma.userRole.create({ + data: { + code: 'ADMIN', + name: 'Administrátor', + level: 2, + permissions: { + projects: ['create', 'read', 'update', 'delete', 'all'], + tasks: ['create', 'read', 'update', 'delete', 'all'], + equipment: ['create', 'read', 'update', 'delete', 'all'], + rma: ['create', 'read', 'update', 'delete', 'approve'], + customers: ['create', 'read', 'update', 'delete'], + users: ['read'], + }, + }, + }), + prisma.userRole.create({ + data: { + code: 'USER', + name: 'Používateľ', + level: 3, + permissions: { + projects: ['read:own', 'update:own'], + tasks: ['create', 'read:assigned', 'update:assigned'], + equipment: ['read', 'update:assigned'], + rma: ['create', 'read:assigned', 'update:assigned'], + customers: ['read'], + }, + }, + }), + prisma.userRole.create({ + data: { + code: 'CUSTOMER', + name: 'Zákazník', + level: 4, + permissions: { + projects: ['read:own'], + tasks: ['read:own'], + equipment: ['read:own'], + rma: ['create:own', 'read:own'], + }, + }, + }), + ]); + + // ===== EQUIPMENT TYPES ===== + await prisma.equipmentType.createMany({ + data: [ + { code: 'EPS', name: 'Elektrická požiarna signalizácia', color: '#3B82F6', order: 1 }, + { code: 'HSP', name: 'Hasiaci systém', color: '#EF4444', order: 2 }, + { code: 'CAMERA', name: 'Kamerový systém', color: '#10B981', order: 3 }, + { code: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 }, + { code: 'OTHER', name: 'Iné zariadenie', color: '#6B7280', order: 5 }, + ], + }); + + // ===== REVISION TYPES ===== + await prisma.revisionType.createMany({ + data: [ + { code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 1 }, + { code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 2 }, + { code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 3 }, + { code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#DC2626', order: 4 }, + ], + }); + + // ===== RMA STATUSES ===== + await prisma.rmaStatus.createMany({ + data: [ + { + code: 'NEW', + name: 'Nová reklamácia', + color: '#10B981', + isInitial: true, + canTransitionTo: JSON.stringify(['IN_ASSESSMENT', 'REJECTED']), + order: 1 + }, + { + code: 'IN_ASSESSMENT', + name: 'V posúdzovaní', + color: '#F59E0B', + canTransitionTo: JSON.stringify(['APPROVED', 'REJECTED']), + order: 2 + }, + { + code: 'APPROVED', + name: 'Schválená', + color: '#3B82F6', + canTransitionTo: JSON.stringify(['IN_REPAIR', 'REPLACED', 'REFUNDED']), + order: 3 + }, + { + code: 'REJECTED', + name: 'Zamietnutá', + color: '#EF4444', + isFinal: true, + order: 4 + }, + { + code: 'IN_REPAIR', + name: 'V oprave', + color: '#8B5CF6', + canTransitionTo: JSON.stringify(['REPAIRED', 'COMPLETED']), + order: 5 + }, + { + code: 'REPAIRED', + name: 'Opravené', + color: '#059669', + canTransitionTo: JSON.stringify(['COMPLETED']), + order: 6 + }, + { + code: 'REPLACED', + name: 'Vymenené', + color: '#059669', + canTransitionTo: JSON.stringify(['COMPLETED']), + order: 7 + }, + { + code: 'REFUNDED', + name: 'Vrátené peniaze', + color: '#059669', + canTransitionTo: JSON.stringify(['COMPLETED']), + order: 8 + }, + { + code: 'COMPLETED', + name: 'Uzatvorená', + color: '#059669', + isFinal: true, + order: 9 + }, + ], + }); + + // ===== RMA SOLUTIONS ===== + await prisma.rmaSolution.createMany({ + data: [ + { code: 'ASSESSMENT', name: 'Posúdzovanie', color: '#F59E0B', order: 1 }, + { code: 'REPAIR', name: 'Oprava', color: '#3B82F6', order: 2 }, + { code: 'REPLACEMENT', name: 'Výmena', color: '#10B981', order: 3 }, + { code: 'REFUND', name: 'Vrátenie peňazí', color: '#8B5CF6', order: 4 }, + { code: 'REJECTED', name: 'Zamietnutie', color: '#EF4444', order: 5 }, + { code: 'OTHER', name: 'Iné riešenie', color: '#6B7280', order: 6 }, + ], + }); + + // ===== TASK STATUSES ===== + await prisma.taskStatus.createMany({ + data: [ + { + code: 'NEW', + name: 'Nová úloha', + swimlaneColumn: 'NEW', + color: '#10B981', + isInitial: true, + order: 1 + }, + { + code: 'IN_PROGRESS', + name: 'V riešení', + swimlaneColumn: 'DOING', + color: '#F59E0B', + order: 2 + }, + { + code: 'COMPLETED', + name: 'Dokončená', + swimlaneColumn: 'DONE', + color: '#059669', + isFinal: true, + order: 3 + }, + ], + }); + + // ===== PRIORITIES ===== + await prisma.priority.createMany({ + data: [ + { code: 'LOW', name: 'Nízka priorita', color: '#10B981', level: 1, order: 1 }, + { code: 'MEDIUM', name: 'Stredná priorita', color: '#F59E0B', level: 5, order: 2 }, + { code: 'HIGH', name: 'Vysoká priorita', color: '#EF4444', level: 8, order: 3 }, + { code: 'URGENT', name: 'Urgentná', color: '#DC2626', level: 10, order: 4 }, + ], + }); + + // ===== SYSTEM SETTINGS ===== + await prisma.systemSetting.createMany({ + data: [ + { + key: 'REVISION_REMINDER_DAYS', + value: JSON.stringify(14), + category: 'NOTIFICATIONS', + label: 'Pripomenúť revíziu X dní dopredu', + dataType: 'number', + validation: JSON.stringify({ min: 1, max: 365 }), + }, + { + key: 'RMA_NUMBER_FORMAT', + value: JSON.stringify('RMA-{YYYY}{MM}{DD}{XXX}'), + category: 'RMA', + label: 'Formát RMA čísla', + dataType: 'string', + }, + { + key: 'RMA_CUSTOMER_REQUIRES_APPROVAL', + value: JSON.stringify(true), + category: 'RMA', + label: 'Reklamácie od zákazníkov vyžadujú schválenie', + dataType: 'boolean', + }, + { + key: 'ADMIN_NOTIFICATION_EMAILS', + value: JSON.stringify(['admin@firma.sk']), + category: 'NOTIFICATIONS', + label: 'Email adresy pre admin notifikácie', + dataType: 'json', + }, + { + key: 'ENABLE_WEBSOCKET', + value: JSON.stringify(false), + category: 'GENERAL', + label: 'Zapnúť real-time aktualizácie (WebSocket)', + dataType: 'boolean', + }, + ], + }); + + // ===== DEMO USERS ===== + const rootRole = roles.find(r => r.code === 'ROOT'); + const adminRole = roles.find(r => r.code === 'ADMIN'); + const userRole = roles.find(r => r.code === 'USER'); + + await prisma.user.createMany({ + data: [ + { + email: 'root@helpdesk.sk', + password: await bcrypt.hash('Root1234!', 10), + name: 'Root Admin', + roleId: rootRole.id, + }, + { + email: 'admin@helpdesk.sk', + password: await bcrypt.hash('Admin1234!', 10), + name: 'Peter Admin', + roleId: adminRole.id, + }, + { + email: 'user@helpdesk.sk', + password: await bcrypt.hash('User1234!', 10), + name: 'Martin Používateľ', + roleId: userRole.id, + }, + ], + }); + + console.log('✅ Seeding completed!'); +} + +seed() + .catch(console.error) + .finally(() => prisma.$disconnect()); +``` + +--- + +## 🔌 Backend Logic Examples + +### **1. Config Service (Caching)** + +```typescript +// services/config.service.ts + +class ConfigService { + private cache = new Map(); + private cacheExpiry = 60000; // 1 minute + + async getEquipmentTypes(activeOnly = true) { + const cacheKey = `equipment_types_${activeOnly}`; + + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey); + } + + const types = await prisma.equipmentType.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.cache.set(cacheKey, types); + setTimeout(() => this.cache.delete(cacheKey), this.cacheExpiry); + + return types; + } + + async getSetting(key: string): Promise { + const cacheKey = `setting_${key}`; + + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey); + } + + const setting = await prisma.systemSetting.findUnique({ + where: { key }, + }); + + if (setting) { + this.cache.set(cacheKey, setting.value); + setTimeout(() => this.cache.delete(cacheKey), this.cacheExpiry); + return setting.value as T; + } + + return null; + } + + clearCache() { + this.cache.clear(); + } +} + +export const configService = new ConfigService(); +``` + +--- + +### **2. RMA Number Generator** + +```typescript +// services/rma.service.ts + +async function generateRMANumber(): Promise { + const format = await configService.getSetting('RMA_NUMBER_FORMAT'); + + if (!format) { + throw new Error('RMA_NUMBER_FORMAT not configured'); + } + + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + + // Count today's RMAs + const count = await prisma.rma.count({ + where: { + createdAt: { + gte: new Date(today.setHours(0, 0, 0, 0)), + lte: new Date(today.setHours(23, 59, 59, 999)), + }, + }, + }); + + const sequence = String(count + 1).padStart(2, '0'); + + // Replace placeholders + let rmaNumber = format + .replace('{YYYY}', String(year)) + .replace('{YY}', String(year).slice(-2)) + .replace('{MM}', month) + .replace('{DD}', day) + .replace('{XXX}', sequence.padStart(3, '0')) + .replace('{XX}', sequence); + + return rmaNumber; +} +``` + +--- + +### **3. RMA Workflow Logic** + +```typescript +// services/rma.service.ts + +async function createRMA(data: CreateRMAInput, userId: string) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { role: true } + }); + + // Check if requires approval (from settings) + let requiresApproval = false; + + if (user.role.code === 'CUSTOMER') { + const setting = await configService.getSetting( + 'RMA_CUSTOMER_REQUIRES_APPROVAL' + ); + requiresApproval = setting === true; + } + + // Get initial status + const initialStatus = await prisma.rmaStatus.findFirst({ + where: { isInitial: true, active: true }, + }); + + if (!initialStatus) { + throw new Error('No initial RMA status configured'); + } + + // Get default solution + const defaultSolution = await prisma.rmaSolution.findFirst({ + where: { code: 'ASSESSMENT', active: true }, + }); + + // Generate RMA number + const rmaNumber = await generateRMANumber(); + + // Create RMA + const rma = await prisma.rma.create({ + data: { + rmaNumber, + statusId: initialStatus.id, + proposedSolutionId: defaultSolution?.id, + requiresApproval, + createdById: userId, + ...data, + }, + include: { + status: true, + customer: true, + createdBy: true, + }, + }); + + // Log activity + await prisma.activityLog.create({ + data: { + userId, + action: 'CREATE', + entity: 'RMA', + entityId: rma.id, + changes: { rmaNumber, status: initialStatus.code }, + }, + }); + + // Create initial status history + await prisma.rmaStatusHistory.create({ + data: { + rmaId: rma.id, + toStatusId: initialStatus.id, + changedById: userId, + notes: 'Reklamácia vytvorená', + }, + }); + + // Notify admins if requires approval + if (requiresApproval) { + await notifyAdminsAboutNewRMA(rma); + } + + return rma; +} + +async function changeRMAStatus( + rmaId: string, + newStatusId: string, + userId: string, + notes?: string +) { + const rma = await prisma.rma.findUnique({ + where: { id: rmaId }, + include: { status: true }, + }); + + if (!rma) { + throw new Error('RMA not found'); + } + + const newStatus = await prisma.rmaStatus.findUnique({ + where: { id: newStatusId }, + }); + + if (!newStatus) { + throw new Error('Status not found'); + } + + // Validate transition + const currentStatus = rma.status; + const allowedTransitions = currentStatus.canTransitionTo as string[]; + + if (allowedTransitions && !allowedTransitions.includes(newStatus.code)) { + throw new Error( + `Cannot transition from ${currentStatus.name} to ${newStatus.name}` + ); + } + + // Update RMA + const updatedRMA = await prisma.rma.update({ + where: { id: rmaId }, + data: { + statusId: newStatusId, + closedAt: newStatus.isFinal ? new Date() : null, + }, + }); + + // Log status change + await prisma.rmaStatusHistory.create({ + data: { + rmaId, + fromStatusId: currentStatus.id, + toStatusId: newStatusId, + changedById: userId, + notes, + }, + }); + + // Activity log + await prisma.activityLog.create({ + data: { + userId, + action: 'STATUS_CHANGE', + entity: 'RMA', + entityId: rmaId, + changes: { + from: currentStatus.code, + to: newStatus.code, + }, + }, + }); + + return updatedRMA; +} +``` + +--- + +### **4. Revision Auto-Calculation** + +```typescript +// services/revision.service.ts + +async function createRevision(data: CreateRevisionInput) { + const revisionType = await prisma.revisionType.findUnique({ + where: { id: data.typeId }, + }); + + if (!revisionType) { + throw new Error('Revision type not found'); + } + + // Auto-calculate nextDueDate + const nextDueDate = addDays( + data.performedDate, + revisionType.intervalDays + ); + + // Auto-calculate reminderDate + const reminderDate = subDays( + nextDueDate, + revisionType.reminderDays + ); + + // Create revision + const revision = await prisma.revision.create({ + data: { + ...data, + nextDueDate, + reminderDate, + }, + include: { + equipment: true, + type: true, + }, + }); + + // Schedule reminder job + await scheduleRevisionReminder(revision.id, reminderDate); + + // Log activity + await prisma.activityLog.create({ + data: { + userId: data.performedById, + action: 'CREATE', + entity: 'Revision', + entityId: revision.id, + changes: { + equipmentId: data.equipmentId, + type: revisionType.code, + nextDue: nextDueDate, + }, + }, + }); + + return revision; +} + +async function getUpcomingRevisions(days: number = 30) { + const futureDate = addDays(new Date(), days); + + return prisma.revision.findMany({ + where: { + nextDueDate: { + gte: new Date(), + lte: futureDate, + }, + reminderSent: false, + }, + include: { + equipment: { + include: { + customer: true, + type: true, + }, + }, + type: true, + }, + orderBy: { + nextDueDate: 'asc', + }, + }); +} +``` + +--- + +### **5. External DB Import (Customer)** + +```typescript +// services/import.service.ts + +interface ExternalCustomer { + id: string; + name: string; + address?: string; + email?: string; + phone?: string; + ico?: string; +} + +async function importCustomersFromExternalDB() { + const dbType = process.env.EXTERNAL_DB_TYPE; + + if (!dbType) { + throw new Error('External DB not configured'); + } + + // Connect to external DB (example: MySQL) + const externalDB = await createExternalConnection(dbType); + + try { + // Query external DB + const externalCustomers: ExternalCustomer[] = await externalDB.query( + 'SELECT id, name, address, email, phone, ico FROM customers' + ); + + console.log(`Found ${externalCustomers.length} customers in external DB`); + + // Import to our DB + const imported = []; + + for (const extCustomer of externalCustomers) { + // Check if already exists + const existing = await prisma.customer.findUnique({ + where: { externalId: extCustomer.id }, + }); + + if (existing) { + // Update existing + await prisma.customer.update({ + where: { id: existing.id }, + data: { + name: extCustomer.name, + address: extCustomer.address, + email: extCustomer.email, + phone: extCustomer.phone, + ico: extCustomer.ico, + }, + }); + } else { + // Create new + const customer = await prisma.customer.create({ + data: { + name: extCustomer.name, + address: extCustomer.address, + email: extCustomer.email, + phone: extCustomer.phone, + ico: extCustomer.ico, + externalId: extCustomer.id, + externalSource: dbType.toUpperCase(), + createdById: 'system', // System user ID + }, + }); + + imported.push(customer); + } + } + + return { + total: externalCustomers.length, + imported: imported.length, + updated: externalCustomers.length - imported.length, + }; + + } finally { + await externalDB.close(); + } +} + +// Cron job (Fáza 3) +cron.schedule('0 2 * * *', async () => { + // Daily at 2 AM + console.log('🔄 Starting customer import...'); + const result = await importCustomersFromExternalDB(); + console.log('✅ Import completed:', result); +}); +``` + +--- + +## 📦 Projektová Štruktúra (Rozšírená) + +``` +helpdesk-system/ +│ +├── backend/ +│ ├── src/ +│ │ ├── config/ +│ │ │ ├── database.ts +│ │ │ ├── jwt.ts +│ │ │ ├── email.ts # NEW +│ │ │ ├── external-db.ts # NEW +│ │ │ └── env.ts +│ │ │ +│ │ ├── controllers/ +│ │ │ ├── auth.controller.ts +│ │ │ ├── users.controller.ts +│ │ │ ├── projects.controller.ts +│ │ │ ├── tasks.controller.ts +│ │ │ ├── customers.controller.ts # NEW +│ │ │ ├── equipment.controller.ts # NEW +│ │ │ ├── revisions.controller.ts # NEW +│ │ │ ├── rma.controller.ts # NEW +│ │ │ ├── settings.controller.ts # NEW +│ │ │ └── dashboard.controller.ts +│ │ │ +│ │ ├── services/ +│ │ │ ├── auth.service.ts +│ │ │ ├── config.service.ts # NEW +│ │ │ ├── email.service.ts # NEW +│ │ │ ├── import.service.ts # NEW +│ │ │ ├── rma.service.ts # NEW +│ │ │ ├── revision.service.ts # NEW +│ │ │ ├── notification.service.ts +│ │ │ └── logger.service.ts +│ │ │ +│ │ ├── middleware/ +│ │ │ ├── auth.middleware.ts +│ │ │ ├── rbac.middleware.ts # Dynamic permissions +│ │ │ ├── validate.middleware.ts +│ │ │ ├── upload.middleware.ts # NEW +│ │ │ ├── errorHandler.ts +│ │ │ └── activityLog.middleware.ts +│ │ │ +│ │ ├── routes/ +│ │ │ ├── auth.routes.ts +│ │ │ ├── users.routes.ts +│ │ │ ├── projects.routes.ts +│ │ │ ├── tasks.routes.ts +│ │ │ ├── customers.routes.ts # NEW +│ │ │ ├── equipment.routes.ts # NEW +│ │ │ ├── revisions.routes.ts # NEW +│ │ │ ├── rma.routes.ts # NEW +│ │ │ ├── settings.routes.ts # NEW +│ │ │ ├── dashboard.routes.ts +│ │ │ └── index.ts +│ │ │ +│ │ ├── utils/ +│ │ │ ├── validators.ts +│ │ │ ├── helpers.ts +│ │ │ ├── pdf-generator.ts # NEW +│ │ │ └── constants.ts +│ │ │ +│ │ ├── jobs/ # NEW (Fáza 2) +│ │ │ ├── reminder.job.ts +│ │ │ └── import.job.ts +│ │ │ +│ │ └── index.ts +│ │ +│ ├── prisma/ +│ │ ├── schema.prisma +│ │ ├── seed.ts +│ │ └── migrations/ +│ │ +│ ├── tests/ +│ ├── uploads/ # NEW +│ ├── .env.example +│ └── package.json +│ +├── frontend/ +│ ├── src/ +│ │ ├── components/ +│ │ │ ├── auth/ +│ │ │ ├── layout/ +│ │ │ ├── dashboard/ +│ │ │ ├── swimlanes/ +│ │ │ ├── tasks/ +│ │ │ ├── projects/ +│ │ │ ├── customers/ # NEW +│ │ │ ├── equipment/ # NEW +│ │ │ ├── rma/ # NEW +│ │ │ ├── settings/ # NEW +│ │ │ ├── shared/ +│ │ │ └── ui/ +│ │ │ +│ │ ├── pages/ +│ │ │ ├── LoginPage.tsx +│ │ │ ├── DashboardPage.tsx +│ │ │ ├── ProjectsPage.tsx +│ │ │ ├── TasksPage.tsx +│ │ │ ├── CustomersPage.tsx # NEW +│ │ │ ├── EquipmentPage.tsx # NEW +│ │ │ ├── RMAPage.tsx # NEW +│ │ │ ├── SettingsPage.tsx # NEW +│ │ │ └── NotFoundPage.tsx +│ │ │ +│ │ ├── hooks/ +│ │ │ ├── useAuth.ts +│ │ │ ├── useConfig.ts # NEW +│ │ │ ├── useProjects.ts +│ │ │ ├── useTasks.ts +│ │ │ ├── useEquipment.ts # NEW +│ │ │ ├── useRMA.ts # NEW +│ │ │ └── useKeyboard.ts +│ │ │ +│ │ ├── services/ +│ │ │ ├── api.ts +│ │ │ ├── auth.api.ts +│ │ │ ├── projects.api.ts +│ │ │ ├── tasks.api.ts +│ │ │ ├── customers.api.ts # NEW +│ │ │ ├── equipment.api.ts # NEW +│ │ │ ├── rma.api.ts # NEW +│ │ │ └── settings.api.ts # NEW +│ │ │ +│ │ ├── store/ +│ │ │ ├── authStore.ts +│ │ │ ├── configStore.ts # NEW +│ │ │ ├── projectsStore.ts +│ │ │ └── tasksStore.ts +│ │ │ +│ │ ├── types/ +│ │ ├── styles/ +│ │ ├── App.tsx +│ │ └── main.tsx +│ │ +│ ├── tests/ +│ └── package.json +│ +├── docker/ +│ ├── Dockerfile.backend +│ ├── Dockerfile.frontend +│ └── docker-compose.yml +│ +├── monitoring/ # NEW (Self-hosted) +│ ├── prometheus.yml +│ ├── grafana-dashboards/ +│ ├── loki-config.yml +│ └── promtail-config.yml +│ +├── docs/ +│ ├── API.md +│ ├── DATABASE.md +│ ├── SETTINGS.md # NEW +│ ├── DEPLOYMENT.md +│ └── USER_GUIDE.md +│ +├── .github/ +│ └── workflows/ +│ ├── ci.yml +│ └── cd.yml +│ +├── README.md +└── LICENSE +``` + +--- + +## 🐳 Docker Compose (Self-Hosted All-in-One) + +```yaml +version: '3.8' + +services: + # ===== DATABASE ===== + postgres: + image: postgres:16-alpine + restart: always + environment: + POSTGRES_DB: helpdesk + POSTGRES_USER: helpdesk + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backups:/backups + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U helpdesk"] + interval: 10s + timeout: 5s + retries: 5 + + # ===== CACHE ===== + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes + volumes: + - redis_data:/data + ports: + - "6379:6379" + + # ===== BACKEND ===== + backend: + build: ./backend + restart: always + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + environment: + DATABASE_URL: postgresql://helpdesk:${DB_PASSWORD}@postgres:5432/helpdesk + REDIS_URL: redis://redis:6379 + JWT_SECRET: ${JWT_SECRET} + NODE_ENV: production + volumes: + - ./uploads:/app/uploads + ports: + - "3001:3001" + + # ===== FRONTEND ===== + frontend: + image: nginx:alpine + restart: always + volumes: + - ./frontend/dist:/usr/share/nginx/html + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + ports: + - "80:80" + - "443:443" + + # ===== EMAIL (Self-hosted) ===== + postfix: + image: boky/postfix:latest + restart: always + environment: + ALLOWED_SENDER_DOMAINS: vasadomena.sk + ports: + - "25:25" + + # ===== MONITORING ===== + prometheus: + image: prom/prometheus:latest + restart: always + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:latest + restart: always + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana-dashboards:/etc/grafana/provisioning/dashboards + environment: + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD} + ports: + - "3000:3000" + + loki: + image: grafana/loki:latest + restart: always + volumes: + - loki_data:/loki + - ./monitoring/loki-config.yml:/etc/loki/local-config.yaml + ports: + - "3100:3100" + + promtail: + image: grafana/promtail:latest + restart: always + volumes: + - /var/log:/var/log:ro + - ./monitoring/promtail-config.yml:/etc/promtail/config.yml + + node-exporter: + image: prom/node-exporter:latest + restart: always + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + restart: always + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + +volumes: + postgres_data: + redis_data: + prometheus_data: + grafana_data: + loki_data: +``` + +--- + +## 📊 Metriky Úspechu + +### **MVP (Fáza 1):** +- [ ] Používateľ sa vie prihlásiť/odhlásiť +- [ ] ROOT vie konfigurovať všetky typy cez Settings +- [ ] Vytvoriť projekt, úlohu, zákazníka +- [ ] Vytvoriť zariadenie (basic) +- [ ] Vytvoriť RMA (basic) +- [ ] Activity log funguje +- [ ] RBAC s dynamickými rolami + +### **Fáza 2:** +- [ ] Swimlanes board funguje +- [ ] Revízny systém s auto-kalkuláciou +- [ ] RMA workflow s approval +- [ ] Email notifikácie +- [ ] File uploads +- [ ] WebSocket updates + +### **Fáza 3:** +- [ ] Import zákazníkov z externej DB +- [ ] Export PDF/Excel +- [ ] Komplexné reporty +- [ ] Performance optimalizovaný + +--- + +## 🔧 Náklady (Self-Hosted) + +``` +VPS/Vlastný server: €0 (vlastný hardware) +Elektrina: ~€10/mesiac +Doména: €10/rok +SSL: €0 (Let's Encrypt) +Monitoring: €0 (self-hosted) +Email: €0 (Postfix) +Backup: €0 (cron + rsync) + +CELKOM: ~€10-15/mesiac +``` + +--- + +## ✅ Záver + +**Helpdesk System V2** je: + +✅ **Plne konfigurovateľný** - žiadne hardcoded hodnoty +✅ **Dynamický** - zmeny cez GUI, žiadne migrácie +✅ **Škálovateľný** - pripravený na rast +✅ **Self-hosted** - 100% kontrola, žiadne závislosti +✅ **Bezpečný** - RBAC, audit log, JWT +✅ **Moderný** - TypeScript, React, Prisma + +**Next Steps:** +1. Vytvorte repozitár +2. Inicializujte backend (Fáza 1) +3. Inicializujte frontend (Fáza 1) +4. Deploy s Docker Compose +5. Testujte a iterujte + +**Good luck! 🚀** + +--- + +*Dokument vytvorený: 02.02.2026* +*Verzia: 2.0.0* +*Autor: Claude (Anthropic) + Používateľ* diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..24d1153 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,19 @@ +# Database +DATABASE_URL="postgresql://user:password@localhost:5432/helpdesk_db" + +# JWT +JWT_SECRET="your-super-secret-key-change-this" +JWT_REFRESH_SECRET="your-refresh-secret-change-this" +JWT_EXPIRES_IN="15m" +JWT_REFRESH_EXPIRES_IN="7d" + +# Server +PORT=3001 +NODE_ENV="development" + +# Frontend URL (for CORS) +FRONTEND_URL="http://localhost:5173" + +# File Upload +UPLOAD_DIR="./uploads" +MAX_FILE_SIZE=10485760 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..c431f65 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local +.env.*.local + +# Prisma +prisma/migrations/ + +# Logs +*.log +npm-debug.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Uploads (keep folder structure) +uploads/* +!uploads/.gitkeep + +# Test coverage +coverage/ diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..ad65ae8 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: helpdesk-postgres + restart: unless-stopped + environment: + POSTGRES_DB: helpdesk_db + POSTGRES_USER: helpdesk + POSTGRES_PASSWORD: helpdesk123 + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U helpdesk -d helpdesk_db"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..221e260 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2092 @@ +{ + "name": "helpdesk-backend", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "helpdesk-backend", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.22.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "morgan": "^1.10.0", + "prisma": "^5.22.0", + "uuid": "^13.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/node": "^25.2.0", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ts-node-dev/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..b2dd942 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "helpdesk-backend", + "version": "2.0.0", + "description": "Helpdesk & Task Management System Backend", + "main": "dist/index.js", + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:seed": "ts-node prisma/seed.ts", + "db:studio": "prisma studio" + }, + "keywords": [ + "helpdesk", + "task-management", + "typescript", + "express", + "prisma" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.22.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "morgan": "^1.10.0", + "prisma": "^5.22.0", + "uuid": "^13.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/node": "^25.2.0", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.3" + } +} diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..9b1958f --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,709 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ==================== USER ROLES (dynamicke) ==================== + +model UserRole { + id String @id @default(cuid()) + code String @unique + name String + description String? + + permissions Json + + level Int + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + + @@index([active]) + @@index([level]) +} + +// ==================== USERS ==================== + +model User { + id String @id @default(cuid()) + email String @unique + password String + name String + + roleId String + role UserRole @relation(fields: [roleId], references: [id]) + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + ownedProjects Project[] @relation("ProjectOwner") + assignedProjects ProjectMember[] + createdTasks Task[] @relation("TaskCreator") + assignedTasks TaskAssignee[] + reminders Reminder[] + activityLogs ActivityLog[] + + createdEquipment Equipment[] @relation("EquipmentCreator") + performedRevisions Revision[] + uploadedEquipmentFiles EquipmentAttachment[] + + assignedRMAs RMA[] @relation("RMAAssignee") + createdRMAs RMA[] @relation("RMACreator") + approvedRMAs RMA[] @relation("RMAApprover") + rmaAttachments RMAAttachment[] + rmaStatusChanges RMAStatusHistory[] + rmaComments RMAComment[] + taskComments Comment[] + + createdCustomers Customer[] + + @@index([email]) + @@index([roleId]) + @@index([active]) +} + +// ==================== CONFIGURATION TABLES ==================== + +model EquipmentType { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + icon String? + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + equipment Equipment[] + + @@index([active]) + @@index([order]) +} + +model RevisionType { + id String @id @default(cuid()) + code String @unique + name String + intervalDays Int + reminderDays Int @default(14) + color String? + description String? + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + revisions Revision[] + + @@index([active]) + @@index([order]) +} + +model RMAStatus { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + icon String? + order Int @default(0) + + isInitial Boolean @default(false) + isFinal Boolean @default(false) + canTransitionTo Json? + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rmas RMA[] + + @@index([active]) + @@index([order]) + @@index([isInitial]) + @@index([isFinal]) +} + +model RMASolution { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rmas RMA[] + + @@index([active]) + @@index([order]) +} + +model TaskStatus { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + icon String? + order Int @default(0) + + swimlaneColumn String? + + isInitial Boolean @default(false) + isFinal Boolean @default(false) + canTransitionTo Json? + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tasks Task[] + projects Project[] + + @@index([active]) + @@index([swimlaneColumn]) + @@index([order]) +} + +model Priority { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + icon String? + level Int + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tasks Task[] + + @@index([active]) + @@index([level]) + @@index([order]) +} + +model Tag { + id String @id @default(cuid()) + code String @unique + name String + description String? + color String? + + entityType String + + order Int @default(0) + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + projectTags ProjectTag[] + taskTags TaskTag[] + equipmentTags EquipmentTag[] + rmaTags RMATag[] + + @@index([entityType]) + @@index([active]) +} + +model SystemSetting { + id String @id @default(cuid()) + + key String @unique + value Json + + category String + label String + description String? + dataType String + + validation Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([category]) +} + +// ==================== CUSTOMERS ==================== + +model Customer { + id String @id @default(cuid()) + + name String + address String? + email String? + phone String? + ico String? + dic String? + icdph String? + + contactPerson String? + contactEmail String? + contactPhone String? + + externalId String? @unique + externalSource String? + + notes String? + active Boolean @default(true) + + createdById String + createdBy User @relation(fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + projects Project[] + equipment Equipment[] + rmas RMA[] + + @@index([name]) + @@index([ico]) + @@index([externalId]) + @@index([active]) +} + +// ==================== PROJECTS ==================== + +model Project { + id String @id @default(cuid()) + name String + description String? + + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + ownerId String + owner User @relation("ProjectOwner", fields: [ownerId], references: [id]) + + statusId String + status TaskStatus @relation(fields: [statusId], references: [id]) + + softDeadline DateTime? + hardDeadline DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + tasks Task[] + members ProjectMember[] + tags ProjectTag[] + + @@index([ownerId]) + @@index([statusId]) + @@index([customerId]) + @@index([hardDeadline]) +} + +model ProjectMember { + id String @id @default(cuid()) + projectId String + userId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + addedAt DateTime @default(now()) + + @@unique([projectId, userId]) + @@index([projectId]) + @@index([userId]) +} + +model ProjectTag { + projectId String + tagId String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([projectId, tagId]) +} + +// ==================== TASKS ==================== + +model Task { + id String @id @default(cuid()) + title String + description String? + + projectId String? + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) + + parentId String? + parent Task? @relation("SubTasks", fields: [parentId], references: [id], onDelete: Cascade) + subTasks Task[] @relation("SubTasks") + + statusId String + status TaskStatus @relation(fields: [statusId], references: [id]) + + priorityId String + priority Priority @relation(fields: [priorityId], references: [id]) + + deadline DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + createdById String + createdBy User @relation("TaskCreator", fields: [createdById], references: [id]) + + assignees TaskAssignee[] + reminders Reminder[] + comments Comment[] + tags TaskTag[] + + @@index([projectId]) + @@index([parentId]) + @@index([statusId]) + @@index([priorityId]) + @@index([deadline]) + @@index([createdById]) +} + +model TaskAssignee { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + assignedAt DateTime @default(now()) + + @@unique([taskId, userId]) + @@index([taskId]) + @@index([userId]) +} + +model TaskTag { + taskId String + tagId String + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([taskId, tagId]) +} + +model Reminder { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + remindAt DateTime + snoozedUntil DateTime? + dismissed Boolean @default(false) + + message String? + + createdAt DateTime @default(now()) + + @@index([userId, remindAt]) + @@index([taskId]) +} + +model Comment { + id String @id @default(cuid()) + taskId String + userId String + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + content String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId]) + @@index([userId]) + @@index([createdAt]) +} + +// ==================== EQUIPMENT MANAGEMENT ==================== + +model Equipment { + id String @id @default(cuid()) + + name String + + typeId String + type EquipmentType @relation(fields: [typeId], references: [id]) + + brand String? + model String? + + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + address String + location String? + + partNumber String? + serialNumber String? + + installDate DateTime? + warrantyEnd DateTime? + warrantyStatus String? + + description String? + notes String? + + active Boolean @default(true) + + createdById String + createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + revisions Revision[] + attachments EquipmentAttachment[] + tags EquipmentTag[] + + @@index([typeId]) + @@index([customerId]) + @@index([warrantyEnd]) + @@index([active]) + @@index([createdById]) +} + +model Revision { + id String @id @default(cuid()) + + equipmentId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + + typeId String + type RevisionType @relation(fields: [typeId], references: [id]) + + performedDate DateTime + nextDueDate DateTime? + + performedById String + performedBy User @relation(fields: [performedById], references: [id]) + + findings String? + result String? + notes String? + + reminderSent Boolean @default(false) + reminderDate DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([equipmentId]) + @@index([typeId]) + @@index([performedById]) + @@index([nextDueDate]) + @@index([reminderDate]) +} + +model EquipmentAttachment { + id String @id @default(cuid()) + equipmentId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + + filename String + filepath String + mimetype String + size Int + + uploadedById String + uploadedBy User @relation(fields: [uploadedById], references: [id]) + + uploadedAt DateTime @default(now()) + + @@index([equipmentId]) +} + +model EquipmentTag { + equipmentId String + tagId String + equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([equipmentId, tagId]) +} + +// ==================== RMA (REKLAMACIE) ==================== + +model RMA { + id String @id @default(cuid()) + + rmaNumber String @unique + + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + + customerName String? + customerAddress String? + customerEmail String? + customerPhone String? + customerICO String? + submittedBy String + + productName String + invoiceNumber String? + purchaseDate DateTime? + productNumber String? + serialNumber String? + accessories String? + + issueDescription String + + statusId String + status RMAStatus @relation(fields: [statusId], references: [id]) + + proposedSolutionId String? + proposedSolution RMASolution? @relation(fields: [proposedSolutionId], references: [id]) + + requiresApproval Boolean @default(false) + approvedById String? + approvedBy User? @relation("RMAApprover", fields: [approvedById], references: [id]) + approvedAt DateTime? + + receivedDate DateTime? + receivedLocation String? + internalNotes String? + + resolutionDate DateTime? + resolutionNotes String? + + assignedToId String? + assignedTo User? @relation("RMAAssignee", fields: [assignedToId], references: [id]) + + createdById String + createdBy User @relation("RMACreator", fields: [createdById], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + closedAt DateTime? + + attachments RMAAttachment[] + statusHistory RMAStatusHistory[] + comments RMAComment[] + tags RMATag[] + + @@index([rmaNumber]) + @@index([customerId]) + @@index([statusId]) + @@index([proposedSolutionId]) + @@index([assignedToId]) + @@index([createdById]) + @@index([purchaseDate]) + @@index([receivedDate]) +} + +model RMAAttachment { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + filename String + filepath String + mimetype String + size Int + + uploadedById String + uploadedBy User @relation(fields: [uploadedById], references: [id]) + + uploadedAt DateTime @default(now()) + + @@index([rmaId]) +} + +model RMAStatusHistory { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + fromStatusId String? + toStatusId String + + changedById String + changedBy User @relation(fields: [changedById], references: [id]) + + notes String? + changedAt DateTime @default(now()) + + @@index([rmaId]) + @@index([changedAt]) +} + +model RMAComment { + id String @id @default(cuid()) + rmaId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + + content String + + userId String + user User @relation(fields: [userId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([rmaId]) + @@index([createdAt]) +} + +model RMATag { + rmaId String + tagId String + rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([rmaId, tagId]) +} + +// ==================== ACTIVITY LOG ==================== + +model ActivityLog { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + action String + entity String + entityId String + + changes Json? + ipAddress String? + userAgent String? + + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([entity, entityId]) + @@index([createdAt]) +} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..b5342d9 --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,260 @@ +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function seed() { + console.log('Seeding database...'); + + // ===== USER ROLES ===== + console.log('Creating user roles...'); + const roles = await Promise.all([ + prisma.userRole.upsert({ + where: { code: 'ROOT' }, + update: {}, + create: { + code: 'ROOT', + name: 'Root Správca', + level: 1, + order: 1, + permissions: { + projects: ['*'], + tasks: ['*'], + equipment: ['*'], + rma: ['*'], + customers: ['*'], + settings: ['*'], + users: ['*'], + logs: ['*'], + }, + }, + }), + prisma.userRole.upsert({ + where: { code: 'ADMIN' }, + update: {}, + create: { + code: 'ADMIN', + name: 'Administrátor', + level: 2, + order: 2, + permissions: { + projects: ['create', 'read', 'update', 'delete', 'all'], + tasks: ['create', 'read', 'update', 'delete', 'all'], + equipment: ['create', 'read', 'update', 'delete', 'all'], + rma: ['create', 'read', 'update', 'delete', 'approve'], + customers: ['create', 'read', 'update', 'delete'], + users: ['read'], + }, + }, + }), + prisma.userRole.upsert({ + where: { code: 'USER' }, + update: {}, + create: { + code: 'USER', + name: 'Používateľ', + level: 3, + order: 3, + permissions: { + projects: ['read', 'update'], + tasks: ['create', 'read', 'update'], + equipment: ['read', 'update'], + rma: ['create', 'read', 'update'], + customers: ['read'], + }, + }, + }), + prisma.userRole.upsert({ + where: { code: 'CUSTOMER' }, + update: {}, + create: { + code: 'CUSTOMER', + name: 'Zákazník', + level: 4, + order: 4, + permissions: { + projects: ['read'], + tasks: ['read'], + equipment: ['read'], + rma: ['create', 'read'], + }, + }, + }), + ]); + + // ===== EQUIPMENT TYPES ===== + console.log('Creating equipment types...'); + await prisma.equipmentType.createMany({ + skipDuplicates: true, + data: [ + { code: 'EPS', name: 'Elektrická požiarna signalizácia', color: '#3B82F6', order: 1 }, + { code: 'HSP', name: 'Hasiaci systém', color: '#EF4444', order: 2 }, + { code: 'CAMERA', name: 'Kamerový systém', color: '#10B981', order: 3 }, + { code: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 }, + { code: 'OTHER', name: 'Iné zariadenie', color: '#6B7280', order: 5 }, + ], + }); + + // ===== REVISION TYPES ===== + console.log('Creating revision types...'); + await prisma.revisionType.createMany({ + skipDuplicates: true, + data: [ + { code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 1 }, + { code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 2 }, + { code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 3 }, + { code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#DC2626', order: 4 }, + ], + }); + + // ===== RMA STATUSES ===== + console.log('Creating RMA statuses...'); + await prisma.rMAStatus.createMany({ + skipDuplicates: true, + data: [ + { code: 'NEW', name: 'Nová reklamácia', color: '#10B981', isInitial: true, canTransitionTo: ['IN_ASSESSMENT', 'REJECTED'], order: 1 }, + { code: 'IN_ASSESSMENT', name: 'V posúdzovaní', color: '#F59E0B', canTransitionTo: ['APPROVED', 'REJECTED'], order: 2 }, + { code: 'APPROVED', name: 'Schválená', color: '#3B82F6', canTransitionTo: ['IN_REPAIR', 'REPLACED', 'REFUNDED'], order: 3 }, + { code: 'REJECTED', name: 'Zamietnutá', color: '#EF4444', isFinal: true, order: 4 }, + { code: 'IN_REPAIR', name: 'V oprave', color: '#8B5CF6', canTransitionTo: ['REPAIRED', 'COMPLETED'], order: 5 }, + { code: 'REPAIRED', name: 'Opravené', color: '#059669', canTransitionTo: ['COMPLETED'], order: 6 }, + { code: 'REPLACED', name: 'Vymenené', color: '#059669', canTransitionTo: ['COMPLETED'], order: 7 }, + { code: 'REFUNDED', name: 'Vrátené peniaze', color: '#059669', canTransitionTo: ['COMPLETED'], order: 8 }, + { code: 'COMPLETED', name: 'Uzatvorená', color: '#059669', isFinal: true, order: 9 }, + ], + }); + + // ===== RMA SOLUTIONS ===== + console.log('Creating RMA solutions...'); + await prisma.rMASolution.createMany({ + skipDuplicates: true, + data: [ + { code: 'ASSESSMENT', name: 'Posúdzovanie', color: '#F59E0B', order: 1 }, + { code: 'REPAIR', name: 'Oprava', color: '#3B82F6', order: 2 }, + { code: 'REPLACEMENT', name: 'Výmena', color: '#10B981', order: 3 }, + { code: 'REFUND', name: 'Vrátenie peňazí', color: '#8B5CF6', order: 4 }, + { code: 'REJECTED', name: 'Zamietnutie', color: '#EF4444', order: 5 }, + { code: 'OTHER', name: 'Iné riešenie', color: '#6B7280', order: 6 }, + ], + }); + + // ===== TASK STATUSES ===== + console.log('Creating task statuses...'); + await prisma.taskStatus.createMany({ + skipDuplicates: true, + data: [ + { code: 'NEW', name: 'Nová úloha', swimlaneColumn: 'NEW', color: '#10B981', isInitial: true, order: 1 }, + { code: 'IN_PROGRESS', name: 'V riešení', swimlaneColumn: 'DOING', color: '#F59E0B', order: 2 }, + { code: 'REVIEW', name: 'Na kontrolu', swimlaneColumn: 'DOING', color: '#8B5CF6', order: 3 }, + { code: 'COMPLETED', name: 'Dokončená', swimlaneColumn: 'DONE', color: '#059669', isFinal: true, order: 4 }, + ], + }); + + // ===== PRIORITIES ===== + console.log('Creating priorities...'); + await prisma.priority.createMany({ + skipDuplicates: true, + data: [ + { code: 'LOW', name: 'Nízka priorita', color: '#10B981', level: 1, order: 1 }, + { code: 'MEDIUM', name: 'Stredná priorita', color: '#F59E0B', level: 5, order: 2 }, + { code: 'HIGH', name: 'Vysoká priorita', color: '#EF4444', level: 8, order: 3 }, + { code: 'URGENT', name: 'Urgentná', color: '#DC2626', level: 10, order: 4 }, + ], + }); + + // ===== SYSTEM SETTINGS ===== + console.log('Creating system settings...'); + await prisma.systemSetting.createMany({ + skipDuplicates: true, + data: [ + { + key: 'REVISION_REMINDER_DAYS', + value: 14, + category: 'NOTIFICATIONS', + label: 'Pripomenúť revíziu X dní dopredu', + dataType: 'number', + validation: { min: 1, max: 365 }, + }, + { + key: 'RMA_NUMBER_FORMAT', + value: 'RMA-{YYYY}{MM}{DD}{XXX}', + category: 'RMA', + label: 'Formát RMA čísla', + dataType: 'string', + }, + { + key: 'RMA_CUSTOMER_REQUIRES_APPROVAL', + value: true, + category: 'RMA', + label: 'Reklamácie od zákazníkov vyžadujú schválenie', + dataType: 'boolean', + }, + { + key: 'ADMIN_NOTIFICATION_EMAILS', + value: ['admin@firma.sk'], + category: 'NOTIFICATIONS', + label: 'Email adresy pre admin notifikácie', + dataType: 'json', + }, + { + key: 'ENABLE_WEBSOCKET', + value: false, + category: 'GENERAL', + label: 'Zapnúť real-time aktualizácie (WebSocket)', + dataType: 'boolean', + }, + ], + }); + + // ===== DEMO USERS ===== + console.log('Creating demo users...'); + const rootRole = roles.find(r => r.code === 'ROOT'); + const adminRole = roles.find(r => r.code === 'ADMIN'); + const userRole = roles.find(r => r.code === 'USER'); + + if (rootRole && adminRole && userRole) { + await prisma.user.upsert({ + where: { email: 'root@helpdesk.sk' }, + update: { password: await bcrypt.hash('root123', 10) }, + create: { + email: 'root@helpdesk.sk', + password: await bcrypt.hash('root123', 10), + name: 'Root Admin', + roleId: rootRole.id, + }, + }); + + await prisma.user.upsert({ + where: { email: 'admin@helpdesk.sk' }, + update: { password: await bcrypt.hash('admin123', 10) }, + create: { + email: 'admin@helpdesk.sk', + password: await bcrypt.hash('admin123', 10), + name: 'Peter Admin', + roleId: adminRole.id, + }, + }); + + await prisma.user.upsert({ + where: { email: 'user@helpdesk.sk' }, + update: { password: await bcrypt.hash('user123', 10) }, + create: { + email: 'user@helpdesk.sk', + password: await bcrypt.hash('user123', 10), + name: 'Martin Používateľ', + roleId: userRole.id, + }, + }); + } + + console.log('Seeding completed!'); +} + +seed() + .catch((error) => { + console.error('Seeding failed:', error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts new file mode 100644 index 0000000..57aadf1 --- /dev/null +++ b/backend/src/config/database.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} + +export default prisma; diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts new file mode 100644 index 0000000..2b1d729 --- /dev/null +++ b/backend/src/config/env.ts @@ -0,0 +1,29 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +export const env = { + // Server + PORT: parseInt(process.env.PORT || '3001', 10), + NODE_ENV: process.env.NODE_ENV || 'development', + + // Database + DATABASE_URL: process.env.DATABASE_URL || '', + + // JWT + JWT_SECRET: process.env.JWT_SECRET || 'default-secret', + JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret', + JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '15m', + JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '7d', + + // Frontend + FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:5173', + + // Upload + UPLOAD_DIR: process.env.UPLOAD_DIR || './uploads', + MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || '10485760', 10), + + // Helpers + isDev: process.env.NODE_ENV === 'development', + isProd: process.env.NODE_ENV === 'production', +}; diff --git a/backend/src/config/jwt.ts b/backend/src/config/jwt.ts new file mode 100644 index 0000000..5fe8a0e --- /dev/null +++ b/backend/src/config/jwt.ts @@ -0,0 +1,29 @@ +import jwt from 'jsonwebtoken'; +import { env } from './env'; + +export interface TokenPayload { + userId: string; + email: string; + roleId: string; + roleCode: string; +} + +export const generateAccessToken = (payload: TokenPayload): string => { + return jwt.sign(payload, env.JWT_SECRET, { + expiresIn: '15m', + }); +}; + +export const generateRefreshToken = (payload: TokenPayload): string => { + return jwt.sign(payload, env.JWT_REFRESH_SECRET, { + expiresIn: '7d', + }); +}; + +export const verifyAccessToken = (token: string): TokenPayload => { + return jwt.verify(token, env.JWT_SECRET) as TokenPayload; +}; + +export const verifyRefreshToken = (token: string): TokenPayload => { + return jwt.verify(token, env.JWT_REFRESH_SECRET) as TokenPayload; +}; diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts new file mode 100644 index 0000000..6f0ba9c --- /dev/null +++ b/backend/src/controllers/auth.controller.ts @@ -0,0 +1,131 @@ +import { Request, Response } from 'express'; +import { authService } from '../services/auth.service'; +import { successResponse, errorResponse } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { logActivity } from '../middleware/activityLog.middleware'; + +export const register = async (req: Request, res: Response): Promise => { + try { + const user = await authService.register(req.body); + + await logActivity( + user.id, + 'CREATE', + 'User', + user.id, + { email: user.email, name: user.name }, + req.ip, + req.get('User-Agent') + ); + + successResponse(res, { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + }, 'Registrácia úspešná.', 201); + } catch (error) { + if (error instanceof Error) { + errorResponse(res, error.message, 400); + } else { + errorResponse(res, 'Chyba pri registrácii.', 500); + } + } +}; + +export const login = async (req: Request, res: Response): Promise => { + try { + const { user, tokens } = await authService.login(req.body); + + await logActivity( + user.id, + 'LOGIN', + 'User', + user.id, + { email: user.email }, + req.ip, + req.get('User-Agent') + ); + + successResponse(res, { + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + }, + ...tokens, + }, 'Prihlásenie úspešné.'); + } catch (error) { + if (error instanceof Error) { + errorResponse(res, error.message, 401); + } else { + errorResponse(res, 'Chyba pri prihlásení.', 500); + } + } +}; + +export const refresh = async (req: Request, res: Response): Promise => { + try { + const { refreshToken } = req.body; + + if (!refreshToken) { + errorResponse(res, 'Refresh token je povinný.', 400); + return; + } + + const tokens = await authService.refreshTokens(refreshToken); + + successResponse(res, tokens, 'Tokeny obnovené.'); + } catch (error) { + if (error instanceof Error) { + errorResponse(res, error.message, 401); + } else { + errorResponse(res, 'Chyba pri obnove tokenov.', 500); + } + } +}; + +export const logout = async (req: AuthRequest, res: Response): Promise => { + try { + if (req.user) { + await logActivity( + req.user.userId, + 'LOGOUT', + 'User', + req.user.userId, + { email: req.user.email }, + req.ip, + req.get('User-Agent') + ); + } + + successResponse(res, null, 'Odhlásenie úspešné.'); + } catch { + errorResponse(res, 'Chyba pri odhlásení.', 500); + } +}; + +export const getMe = async (req: AuthRequest, res: Response): Promise => { + try { + if (!req.user) { + errorResponse(res, 'Nie ste prihlásený.', 401); + return; + } + + const user = await authService.getMe(req.user.userId); + + successResponse(res, { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + }); + } catch (error) { + if (error instanceof Error) { + errorResponse(res, error.message, 404); + } else { + errorResponse(res, 'Chyba pri získaní údajov.', 500); + } + } +}; diff --git a/backend/src/controllers/customers.controller.ts b/backend/src/controllers/customers.controller.ts new file mode 100644 index 0000000..6ca428a --- /dev/null +++ b/backend/src/controllers/customers.controller.ts @@ -0,0 +1,216 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; + +export const getCustomers = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const active = getQueryString(req, 'active'); + + const where = { + ...(active !== undefined && { active: active === 'true' }), + ...(search && { + OR: [ + { name: { contains: search, mode: 'insensitive' as const } }, + { email: { contains: search, mode: 'insensitive' as const } }, + { ico: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [customers, total] = await Promise.all([ + prisma.customer.findMany({ + where, + skip, + take: limit, + orderBy: { name: 'asc' }, + include: { + _count: { + select: { + projects: true, + equipment: true, + rmas: true, + }, + }, + }, + }), + prisma.customer.count({ where }), + ]); + + paginatedResponse(res, customers, total, page, limit); + } catch (error) { + console.error('Error fetching customers:', error); + errorResponse(res, 'Chyba pri načítaní zákazníkov.', 500); + } +}; + +export const getCustomer = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const customer = await prisma.customer.findUnique({ + where: { id }, + include: { + createdBy: { + select: { id: true, name: true, email: true }, + }, + _count: { + select: { + projects: true, + equipment: true, + rmas: true, + }, + }, + }, + }); + + if (!customer) { + errorResponse(res, 'Zákazník nebol nájdený.', 404); + return; + } + + successResponse(res, customer); + } catch (error) { + console.error('Error fetching customer:', error); + errorResponse(res, 'Chyba pri načítaní zákazníka.', 500); + } +}; + +export const createCustomer = async (req: AuthRequest, res: Response): Promise => { + try { + const customer = await prisma.customer.create({ + data: { + ...req.body, + createdById: req.user!.userId, + }, + }); + + if (req.logActivity) { + await req.logActivity('CREATE', 'Customer', customer.id, { name: customer.name }); + } + + successResponse(res, customer, 'Zákazník bol vytvorený.', 201); + } catch (error) { + console.error('Error creating customer:', error); + errorResponse(res, 'Chyba pri vytváraní zákazníka.', 500); + } +}; + +export const updateCustomer = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const existing = await prisma.customer.findUnique({ where: { id } }); + + if (!existing) { + errorResponse(res, 'Zákazník nebol nájdený.', 404); + return; + } + + const customer = await prisma.customer.update({ + where: { id }, + data: req.body, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'Customer', id, req.body); + } + + successResponse(res, customer, 'Zákazník bol aktualizovaný.'); + } catch (error) { + console.error('Error updating customer:', error); + errorResponse(res, 'Chyba pri aktualizácii zákazníka.', 500); + } +}; + +export const deleteCustomer = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const customer = await prisma.customer.findUnique({ where: { id } }); + + if (!customer) { + errorResponse(res, 'Zákazník nebol nájdený.', 404); + return; + } + + // Soft delete + await prisma.customer.update({ + where: { id }, + data: { active: false }, + }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'Customer', id); + } + + successResponse(res, null, 'Zákazník bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting customer:', error); + errorResponse(res, 'Chyba pri mazaní zákazníka.', 500); + } +}; + +export const getCustomerProjects = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const projects = await prisma.project.findMany({ + where: { customerId: id }, + include: { + status: true, + owner: { select: { id: true, name: true } }, + _count: { select: { tasks: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + + successResponse(res, projects); + } catch (error) { + console.error('Error fetching customer projects:', error); + errorResponse(res, 'Chyba pri načítaní projektov.', 500); + } +}; + +export const getCustomerEquipment = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const equipment = await prisma.equipment.findMany({ + where: { customerId: id, active: true }, + include: { + type: true, + }, + orderBy: { createdAt: 'desc' }, + }); + + successResponse(res, equipment); + } catch (error) { + console.error('Error fetching customer equipment:', error); + errorResponse(res, 'Chyba pri načítaní zariadení.', 500); + } +}; + +export const getCustomerRMAs = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const rmas = await prisma.rMA.findMany({ + where: { customerId: id }, + include: { + status: true, + proposedSolution: true, + }, + orderBy: { createdAt: 'desc' }, + }); + + successResponse(res, rmas); + } catch (error) { + console.error('Error fetching customer RMAs:', error); + errorResponse(res, 'Chyba pri načítaní reklamácií.', 500); + } +}; diff --git a/backend/src/controllers/dashboard.controller.ts b/backend/src/controllers/dashboard.controller.ts new file mode 100644 index 0000000..9f11b9a --- /dev/null +++ b/backend/src/controllers/dashboard.controller.ts @@ -0,0 +1,333 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; + +// Hlavný dashboard endpoint - štatistiky pre karty +export const getDashboard = async (_req: AuthRequest, res: Response): Promise => { + try { + const today = new Date(); + const nextMonth = new Date(today); + nextMonth.setDate(nextMonth.getDate() + 30); + + const [ + totalProjects, + activeProjects, + totalTasks, + pendingTasks, + inProgressTasks, + totalCustomers, + activeCustomers, + totalEquipment, + upcomingRevisions, + totalRMAs, + pendingRMAs, + ] = await Promise.all([ + prisma.project.count(), + prisma.project.count({ where: { status: { isFinal: false } } }), + prisma.task.count(), + prisma.task.count({ where: { status: { code: 'NEW' } } }), + prisma.task.count({ where: { status: { code: 'IN_PROGRESS' } } }), + prisma.customer.count(), + prisma.customer.count({ where: { active: true } }), + prisma.equipment.count({ where: { active: true } }), + prisma.revision.count({ + where: { + nextDueDate: { + gte: today, + lte: nextMonth, + }, + }, + }), + prisma.rMA.count(), + prisma.rMA.count({ where: { status: { isFinal: false } } }), + ]); + + successResponse(res, { + projects: { + total: totalProjects, + active: activeProjects, + }, + tasks: { + total: totalTasks, + pending: pendingTasks, + inProgress: inProgressTasks, + }, + customers: { + total: totalCustomers, + active: activeCustomers, + }, + equipment: { + total: totalEquipment, + upcomingRevisions, + }, + rma: { + total: totalRMAs, + pending: pendingRMAs, + }, + }); + } catch (error) { + console.error('Error fetching dashboard:', error); + errorResponse(res, 'Chyba pri načítaní dashboardu.', 500); + } +}; + +export const getDashboardToday = async (req: AuthRequest, res: Response): Promise => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const userId = req.user!.userId; + + const [myTasks, myProjects, recentRMAs] = await Promise.all([ + // Tasks assigned to me that are not completed + prisma.task.findMany({ + where: { + assignees: { some: { userId } }, + status: { isFinal: false }, + }, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + assignees: { include: { user: { select: { id: true, name: true } } } }, + }, + orderBy: [{ priority: { level: 'desc' } }, { deadline: 'asc' }], + take: 10, + }), + + // My active projects + prisma.project.findMany({ + where: { + OR: [ + { ownerId: userId }, + { members: { some: { userId } } }, + ], + status: { isFinal: false }, + }, + include: { + status: true, + _count: { select: { tasks: true } }, + }, + take: 5, + }), + + // Recent RMAs + prisma.rMA.findMany({ + where: { + OR: [ + { assignedToId: userId }, + { createdById: userId }, + ], + }, + include: { + status: true, + }, + orderBy: { createdAt: 'desc' }, + take: 5, + }), + ]); + + successResponse(res, { + myTasks, + myProjects, + recentRMAs, + }); + } catch (error) { + console.error('Error fetching dashboard today:', error); + errorResponse(res, 'Chyba pri načítaní dashboardu.', 500); + } +}; + +export const getDashboardWeek = async (req: AuthRequest, res: Response): Promise => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const nextWeek = new Date(today); + nextWeek.setDate(nextWeek.getDate() + 7); + + const userId = req.user!.userId; + + const [tasksDeadlineThisWeek, upcomingRevisions] = await Promise.all([ + // Tasks with deadline this week + prisma.task.findMany({ + where: { + deadline: { + gte: today, + lte: nextWeek, + }, + status: { isFinal: false }, + OR: [ + { createdById: userId }, + { assignees: { some: { userId } } }, + ], + }, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + assignees: { include: { user: { select: { id: true, name: true } } } }, + }, + orderBy: { deadline: 'asc' }, + }), + + // Upcoming equipment revisions + prisma.revision.findMany({ + where: { + nextDueDate: { + gte: today, + lte: nextWeek, + }, + }, + include: { + equipment: { + include: { + type: true, + customer: { select: { id: true, name: true } }, + }, + }, + type: true, + }, + orderBy: { nextDueDate: 'asc' }, + }), + ]); + + successResponse(res, { + tasksDeadlineThisWeek, + upcomingRevisions, + }); + } catch (error) { + console.error('Error fetching dashboard week:', error); + errorResponse(res, 'Chyba pri načítaní týždenného prehľadu.', 500); + } +}; + +export const getDashboardStats = async (req: AuthRequest, res: Response): Promise => { + try { + const [ + totalProjects, + activeProjects, + totalTasks, + completedTasks, + totalCustomers, + totalEquipment, + totalRMAs, + openRMAs, + ] = await Promise.all([ + prisma.project.count(), + prisma.project.count({ where: { status: { isFinal: false } } }), + prisma.task.count(), + prisma.task.count({ where: { status: { isFinal: true } } }), + prisma.customer.count({ where: { active: true } }), + prisma.equipment.count({ where: { active: true } }), + prisma.rMA.count(), + prisma.rMA.count({ where: { status: { isFinal: false } } }), + ]); + + successResponse(res, { + projects: { + total: totalProjects, + active: activeProjects, + }, + tasks: { + total: totalTasks, + completed: completedTasks, + completionRate: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0, + }, + customers: { + total: totalCustomers, + }, + equipment: { + total: totalEquipment, + }, + rmas: { + total: totalRMAs, + open: openRMAs, + }, + }); + } catch (error) { + console.error('Error fetching dashboard stats:', error); + errorResponse(res, 'Chyba pri načítaní štatistík.', 500); + } +}; + +export const getDashboardReminders = async (req: AuthRequest, res: Response): Promise => { + try { + const today = new Date(); + const nextMonth = new Date(today); + nextMonth.setDate(nextMonth.getDate() + 30); + + const userId = req.user!.userId; + + const [taskReminders, equipmentRevisions, overdueRMAs] = await Promise.all([ + // Task reminders + prisma.reminder.findMany({ + where: { + userId, + dismissed: false, + remindAt: { + lte: nextMonth, + }, + }, + include: { + task: { + include: { + status: true, + project: { select: { id: true, name: true } }, + }, + }, + }, + orderBy: { remindAt: 'asc' }, + }), + + // Equipment revision reminders + prisma.revision.findMany({ + where: { + nextDueDate: { + gte: today, + lte: nextMonth, + }, + }, + include: { + equipment: { + include: { + type: true, + customer: { select: { id: true, name: true } }, + }, + }, + type: true, + }, + orderBy: { nextDueDate: 'asc' }, + take: 10, + }), + + // Overdue or old RMAs + prisma.rMA.findMany({ + where: { + status: { isFinal: false }, + createdAt: { + lte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000), // Older than 7 days + }, + }, + include: { + status: true, + customer: { select: { id: true, name: true } }, + }, + orderBy: { createdAt: 'asc' }, + take: 10, + }), + ]); + + successResponse(res, { + taskReminders, + equipmentRevisions, + overdueRMAs, + }); + } catch (error) { + console.error('Error fetching dashboard reminders:', error); + errorResponse(res, 'Chyba pri načítaní upomienok.', 500); + } +}; diff --git a/backend/src/controllers/equipment.controller.ts b/backend/src/controllers/equipment.controller.ts new file mode 100644 index 0000000..69e1136 --- /dev/null +++ b/backend/src/controllers/equipment.controller.ts @@ -0,0 +1,313 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; + +export const getEquipment = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const active = getQueryString(req, 'active'); + const typeId = getQueryString(req, 'typeId'); + const customerId = getQueryString(req, 'customerId'); + + const where = { + ...(active !== undefined && { active: active === 'true' }), + ...(typeId && { typeId }), + ...(customerId && { customerId }), + ...(search && { + OR: [ + { name: { contains: search, mode: 'insensitive' as const } }, + { serialNumber: { contains: search, mode: 'insensitive' as const } }, + { address: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [equipment, total] = await Promise.all([ + prisma.equipment.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + type: true, + customer: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + _count: { select: { revisions: true } }, + }, + }), + prisma.equipment.count({ where }), + ]); + + paginatedResponse(res, equipment, total, page, limit); + } catch (error) { + console.error('Error fetching equipment:', error); + errorResponse(res, 'Chyba pri načítaní zariadení.', 500); + } +}; + +export const getEquipmentById = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const equipment = await prisma.equipment.findUnique({ + where: { id }, + include: { + type: true, + customer: true, + createdBy: { select: { id: true, name: true, email: true } }, + revisions: { + orderBy: { performedDate: 'desc' }, + take: 5, + include: { + type: true, + performedBy: { select: { id: true, name: true } }, + }, + }, + attachments: { + orderBy: { uploadedAt: 'desc' }, + }, + tags: { include: { tag: true } }, + }, + }); + + if (!equipment) { + errorResponse(res, 'Zariadenie nebolo nájdené.', 404); + return; + } + + successResponse(res, equipment); + } catch (error) { + console.error('Error fetching equipment:', error); + errorResponse(res, 'Chyba pri načítaní zariadenia.', 500); + } +}; + +export const createEquipment = async (req: AuthRequest, res: Response): Promise => { + try { + const equipment = await prisma.equipment.create({ + data: { + name: req.body.name, + typeId: req.body.typeId, + brand: req.body.brand, + model: req.body.model, + customerId: req.body.customerId || null, + address: req.body.address, + location: req.body.location, + partNumber: req.body.partNumber, + serialNumber: req.body.serialNumber, + installDate: req.body.installDate ? new Date(req.body.installDate) : null, + warrantyEnd: req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null, + warrantyStatus: req.body.warrantyStatus, + description: req.body.description, + notes: req.body.notes, + createdById: req.user!.userId, + }, + include: { + type: true, + customer: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('CREATE', 'Equipment', equipment.id, { name: equipment.name }); + } + + successResponse(res, equipment, 'Zariadenie bolo vytvorené.', 201); + } catch (error) { + console.error('Error creating equipment:', error); + errorResponse(res, 'Chyba pri vytváraní zariadenia.', 500); + } +}; + +export const updateEquipment = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const existing = await prisma.equipment.findUnique({ where: { id } }); + + if (!existing) { + errorResponse(res, 'Zariadenie nebolo nájdené.', 404); + return; + } + + const updateData: Record = {}; + const fields = [ + 'name', 'typeId', 'brand', 'model', 'customerId', 'address', + 'location', 'partNumber', 'serialNumber', 'warrantyStatus', + 'description', 'notes', 'active' + ]; + + for (const field of fields) { + if (req.body[field] !== undefined) { + updateData[field] = req.body[field]; + } + } + + if (req.body.installDate !== undefined) { + updateData.installDate = req.body.installDate ? new Date(req.body.installDate) : null; + } + if (req.body.warrantyEnd !== undefined) { + updateData.warrantyEnd = req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null; + } + + const equipment = await prisma.equipment.update({ + where: { id }, + data: updateData, + include: { + type: true, + customer: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'Equipment', id, updateData); + } + + successResponse(res, equipment, 'Zariadenie bolo aktualizované.'); + } catch (error) { + console.error('Error updating equipment:', error); + errorResponse(res, 'Chyba pri aktualizácii zariadenia.', 500); + } +}; + +export const deleteEquipment = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const equipment = await prisma.equipment.findUnique({ where: { id } }); + + if (!equipment) { + errorResponse(res, 'Zariadenie nebolo nájdené.', 404); + return; + } + + // Soft delete + await prisma.equipment.update({ + where: { id }, + data: { active: false }, + }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'Equipment', id); + } + + successResponse(res, null, 'Zariadenie bolo deaktivované.'); + } catch (error) { + console.error('Error deleting equipment:', error); + errorResponse(res, 'Chyba pri mazaní zariadenia.', 500); + } +}; + +export const getEquipmentRevisions = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const revisions = await prisma.revision.findMany({ + where: { equipmentId: id }, + orderBy: { performedDate: 'desc' }, + include: { + type: true, + performedBy: { select: { id: true, name: true } }, + }, + }); + + successResponse(res, revisions); + } catch (error) { + console.error('Error fetching equipment revisions:', error); + errorResponse(res, 'Chyba pri načítaní revízií.', 500); + } +}; + +export const createEquipmentRevision = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const equipment = await prisma.equipment.findUnique({ where: { id } }); + if (!equipment) { + errorResponse(res, 'Zariadenie nebolo nájdené.', 404); + return; + } + + const revisionType = await prisma.revisionType.findUnique({ + where: { id: req.body.typeId }, + }); + + if (!revisionType) { + errorResponse(res, 'Typ revízie nebol nájdený.', 404); + return; + } + + const performedDate = new Date(req.body.performedDate); + const nextDueDate = new Date(performedDate); + nextDueDate.setDate(nextDueDate.getDate() + revisionType.intervalDays); + + const reminderDate = new Date(nextDueDate); + reminderDate.setDate(reminderDate.getDate() - revisionType.reminderDays); + + const revision = await prisma.revision.create({ + data: { + equipmentId: id, + typeId: req.body.typeId, + performedDate, + nextDueDate, + reminderDate, + performedById: req.user!.userId, + findings: req.body.findings, + result: req.body.result, + notes: req.body.notes, + }, + include: { + type: true, + performedBy: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('CREATE', 'Revision', revision.id, { + equipmentId: id, + type: revisionType.name, + }); + } + + successResponse(res, revision, 'Revízia bola vytvorená.', 201); + } catch (error) { + console.error('Error creating revision:', error); + errorResponse(res, 'Chyba pri vytváraní revízie.', 500); + } +}; + +export const getEquipmentReminders = async (req: AuthRequest, res: Response): Promise => { + try { + const days = parseQueryInt(req.query.days, 30); + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + days); + + const revisions = await prisma.revision.findMany({ + where: { + nextDueDate: { + gte: new Date(), + lte: futureDate, + }, + }, + include: { + equipment: { + include: { + type: true, + customer: { select: { id: true, name: true } }, + }, + }, + type: true, + }, + orderBy: { nextDueDate: 'asc' }, + }); + + successResponse(res, revisions); + } catch (error) { + console.error('Error fetching equipment reminders:', error); + errorResponse(res, 'Chyba pri načítaní upomienok.', 500); + } +}; diff --git a/backend/src/controllers/projects.controller.ts b/backend/src/controllers/projects.controller.ts new file mode 100644 index 0000000..103c869 --- /dev/null +++ b/backend/src/controllers/projects.controller.ts @@ -0,0 +1,288 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { configService } from '../services/config.service'; + +export const getProjects = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const statusId = getQueryString(req, 'statusId'); + const customerId = getQueryString(req, 'customerId'); + const ownerId = getQueryString(req, 'ownerId'); + + const where = { + ...(statusId && { statusId }), + ...(customerId && { customerId }), + ...(ownerId && { ownerId }), + ...(search && { + OR: [ + { name: { contains: search, mode: 'insensitive' as const } }, + { description: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [projects, total] = await Promise.all([ + prisma.project.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + status: true, + customer: { select: { id: true, name: true } }, + owner: { select: { id: true, name: true, email: true } }, + _count: { select: { tasks: true, members: true } }, + }, + }), + prisma.project.count({ where }), + ]); + + paginatedResponse(res, projects, total, page, limit); + } catch (error) { + console.error('Error fetching projects:', error); + errorResponse(res, 'Chyba pri načítaní projektov.', 500); + } +}; + +export const getProject = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const project = await prisma.project.findUnique({ + where: { id }, + include: { + status: true, + customer: true, + owner: { select: { id: true, name: true, email: true } }, + members: { + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }, + tags: { include: { tag: true } }, + _count: { select: { tasks: true } }, + }, + }); + + if (!project) { + errorResponse(res, 'Projekt nebol nájdený.', 404); + return; + } + + successResponse(res, project); + } catch (error) { + console.error('Error fetching project:', error); + errorResponse(res, 'Chyba pri načítaní projektu.', 500); + } +}; + +export const createProject = async (req: AuthRequest, res: Response): Promise => { + try { + let { statusId } = req.body; + + // Get initial status if not provided + if (!statusId) { + const initialStatus = await configService.getInitialTaskStatus(); + if (initialStatus) { + statusId = initialStatus.id; + } else { + // Fallback: vezmi prvý aktívny status + const allStatuses = await configService.getTaskStatuses(); + const statuses = allStatuses as { id: string }[]; + if (statuses.length > 0) { + statusId = statuses[0].id; + } else { + errorResponse(res, 'Žiadny status nie je nakonfigurovaný. Spustite seed.', 500); + return; + } + } + } + + const project = await prisma.project.create({ + data: { + name: req.body.name, + description: req.body.description, + customerId: req.body.customerId || null, + ownerId: req.user!.userId, + statusId, + softDeadline: req.body.softDeadline ? new Date(req.body.softDeadline) : null, + hardDeadline: req.body.hardDeadline ? new Date(req.body.hardDeadline) : null, + }, + include: { + status: true, + customer: { select: { id: true, name: true } }, + owner: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('CREATE', 'Project', project.id, { name: project.name }); + } + + successResponse(res, project, 'Projekt bol vytvorený.', 201); + } catch (error) { + console.error('Error creating project:', error); + errorResponse(res, 'Chyba pri vytváraní projektu.', 500); + } +}; + +export const updateProject = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const existing = await prisma.project.findUnique({ where: { id } }); + + if (!existing) { + errorResponse(res, 'Projekt nebol nájdený.', 404); + return; + } + + const updateData: Record = {}; + if (req.body.name) updateData.name = req.body.name; + if (req.body.description !== undefined) updateData.description = req.body.description; + if (req.body.customerId !== undefined) updateData.customerId = req.body.customerId || null; + if (req.body.statusId) updateData.statusId = req.body.statusId; + if (req.body.softDeadline !== undefined) { + updateData.softDeadline = req.body.softDeadline ? new Date(req.body.softDeadline) : null; + } + if (req.body.hardDeadline !== undefined) { + updateData.hardDeadline = req.body.hardDeadline ? new Date(req.body.hardDeadline) : null; + } + + const project = await prisma.project.update({ + where: { id }, + data: updateData, + include: { + status: true, + customer: { select: { id: true, name: true } }, + owner: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'Project', id, updateData); + } + + successResponse(res, project, 'Projekt bol aktualizovaný.'); + } catch (error) { + console.error('Error updating project:', error); + errorResponse(res, 'Chyba pri aktualizácii projektu.', 500); + } +}; + +export const deleteProject = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const project = await prisma.project.findUnique({ where: { id } }); + + if (!project) { + errorResponse(res, 'Projekt nebol nájdený.', 404); + return; + } + + await prisma.project.delete({ where: { id } }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'Project', id); + } + + successResponse(res, null, 'Projekt bol vymazaný.'); + } catch (error) { + console.error('Error deleting project:', error); + errorResponse(res, 'Chyba pri mazaní projektu.', 500); + } +}; + +export const updateProjectStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { statusId } = req.body; + + const project = await prisma.project.update({ + where: { id }, + data: { statusId }, + include: { status: true }, + }); + + if (req.logActivity) { + await req.logActivity('STATUS_CHANGE', 'Project', id, { statusId }); + } + + successResponse(res, project, 'Status projektu bol zmenený.'); + } catch (error) { + console.error('Error updating project status:', error); + errorResponse(res, 'Chyba pri zmene statusu.', 500); + } +}; + +export const getProjectTasks = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const tasks = await prisma.task.findMany({ + where: { projectId: id }, + include: { + status: true, + priority: true, + assignees: { + include: { user: { select: { id: true, name: true } } }, + }, + }, + orderBy: [{ priority: { level: 'desc' } }, { createdAt: 'desc' }], + }); + + successResponse(res, tasks); + } catch (error) { + console.error('Error fetching project tasks:', error); + errorResponse(res, 'Chyba pri načítaní úloh.', 500); + } +}; + +export const addProjectMember = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { userId } = req.body; + + const member = await prisma.projectMember.create({ + data: { + projectId: id, + userId, + }, + include: { + user: { select: { id: true, name: true, email: true } }, + }, + }); + + successResponse(res, member, 'Člen bol pridaný.', 201); + } catch (error) { + console.error('Error adding project member:', error); + errorResponse(res, 'Chyba pri pridávaní člena.', 500); + } +}; + +export const removeProjectMember = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const userId = getParam(req, 'userId'); + + await prisma.projectMember.delete({ + where: { + projectId_userId: { + projectId: id, + userId, + }, + }, + }); + + successResponse(res, null, 'Člen bol odstránený.'); + } catch (error) { + console.error('Error removing project member:', error); + errorResponse(res, 'Chyba pri odstraňovaní člena.', 500); + } +}; diff --git a/backend/src/controllers/rma.controller.ts b/backend/src/controllers/rma.controller.ts new file mode 100644 index 0000000..7a4a3bd --- /dev/null +++ b/backend/src/controllers/rma.controller.ts @@ -0,0 +1,414 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { configService } from '../services/config.service'; + +async function generateRMANumber(): Promise { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + + const startOfDay = new Date(today.setHours(0, 0, 0, 0)); + const endOfDay = new Date(today.setHours(23, 59, 59, 999)); + + const count = await prisma.rMA.count({ + where: { + createdAt: { + gte: startOfDay, + lte: endOfDay, + }, + }, + }); + + const sequence = String(count + 1).padStart(3, '0'); + return `RMA-${year}${month}${day}${sequence}`; +} + +export const getRMAs = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const statusId = getQueryString(req, 'statusId'); + const customerId = getQueryString(req, 'customerId'); + const assignedToId = getQueryString(req, 'assignedToId'); + + const where = { + ...(statusId && { statusId }), + ...(customerId && { customerId }), + ...(assignedToId && { assignedToId }), + ...(search && { + OR: [ + { rmaNumber: { contains: search, mode: 'insensitive' as const } }, + { productName: { contains: search, mode: 'insensitive' as const } }, + { customerName: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [rmas, total] = await Promise.all([ + prisma.rMA.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + status: true, + proposedSolution: true, + customer: { select: { id: true, name: true } }, + assignedTo: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + }, + }), + prisma.rMA.count({ where }), + ]); + + paginatedResponse(res, rmas, total, page, limit); + } catch (error) { + console.error('Error fetching RMAs:', error); + errorResponse(res, 'Chyba pri načítaní reklamácií.', 500); + } +}; + +export const getRMA = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const rma = await prisma.rMA.findUnique({ + where: { id }, + include: { + status: true, + proposedSolution: true, + customer: true, + assignedTo: { select: { id: true, name: true, email: true } }, + createdBy: { select: { id: true, name: true, email: true } }, + approvedBy: { select: { id: true, name: true } }, + attachments: { + orderBy: { uploadedAt: 'desc' }, + }, + statusHistory: { + orderBy: { changedAt: 'desc' }, + include: { + changedBy: { select: { id: true, name: true } }, + }, + }, + comments: { + orderBy: { createdAt: 'desc' }, + include: { + user: { select: { id: true, name: true } }, + }, + }, + tags: { include: { tag: true } }, + }, + }); + + if (!rma) { + errorResponse(res, 'Reklamácia nebola nájdená.', 404); + return; + } + + successResponse(res, rma); + } catch (error) { + console.error('Error fetching RMA:', error); + errorResponse(res, 'Chyba pri načítaní reklamácie.', 500); + } +}; + +export const createRMA = async (req: AuthRequest, res: Response): Promise => { + try { + let { statusId, proposedSolutionId } = req.body; + + // Get initial status if not provided + if (!statusId) { + const initialStatus = await configService.getInitialRMAStatus(); + if (initialStatus) { + statusId = initialStatus.id; + } else { + errorResponse(res, 'Predvolený status reklamácie nie je nakonfigurovaný.', 500); + return; + } + } + + // Get default solution (Assessment) if not provided + if (!proposedSolutionId) { + const solutions = await configService.getRMASolutions(); + const defaultSolution = (solutions as { id: string; code: string }[]).find( + (s) => s.code === 'ASSESSMENT' + ); + if (defaultSolution) { + proposedSolutionId = defaultSolution.id; + } + } + + const rmaNumber = await generateRMANumber(); + + // Check if customer role requires approval + let requiresApproval = false; + if (req.user?.roleCode === 'CUSTOMER') { + const setting = await configService.getSetting('RMA_CUSTOMER_REQUIRES_APPROVAL'); + requiresApproval = setting === true; + } + + const rma = await prisma.rMA.create({ + data: { + rmaNumber, + customerId: req.body.customerId || null, + customerName: req.body.customerName, + customerAddress: req.body.customerAddress, + customerEmail: req.body.customerEmail, + customerPhone: req.body.customerPhone, + customerICO: req.body.customerICO, + submittedBy: req.body.submittedBy, + productName: req.body.productName, + invoiceNumber: req.body.invoiceNumber, + purchaseDate: req.body.purchaseDate ? new Date(req.body.purchaseDate) : null, + productNumber: req.body.productNumber, + serialNumber: req.body.serialNumber, + accessories: req.body.accessories, + issueDescription: req.body.issueDescription, + statusId, + proposedSolutionId, + requiresApproval, + createdById: req.user!.userId, + }, + include: { + status: true, + proposedSolution: true, + customer: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + }, + }); + + // Create initial status history + await prisma.rMAStatusHistory.create({ + data: { + rmaId: rma.id, + toStatusId: statusId, + changedById: req.user!.userId, + notes: 'Reklamácia vytvorená', + }, + }); + + if (req.logActivity) { + await req.logActivity('CREATE', 'RMA', rma.id, { rmaNumber }); + } + + successResponse(res, rma, 'Reklamácia bola vytvorená.', 201); + } catch (error) { + console.error('Error creating RMA:', error); + errorResponse(res, 'Chyba pri vytváraní reklamácie.', 500); + } +}; + +export const updateRMA = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const existing = await prisma.rMA.findUnique({ where: { id } }); + + if (!existing) { + errorResponse(res, 'Reklamácia nebola nájdená.', 404); + return; + } + + const updateData: Record = {}; + const fields = [ + 'customerId', 'customerName', 'customerAddress', 'customerEmail', + 'customerPhone', 'customerICO', 'submittedBy', 'productName', + 'invoiceNumber', 'productNumber', 'serialNumber', 'accessories', + 'issueDescription', 'proposedSolutionId', 'receivedLocation', + 'internalNotes', 'resolutionNotes', 'assignedToId' + ]; + + for (const field of fields) { + if (req.body[field] !== undefined) { + updateData[field] = req.body[field]; + } + } + + if (req.body.purchaseDate !== undefined) { + updateData.purchaseDate = req.body.purchaseDate ? new Date(req.body.purchaseDate) : null; + } + if (req.body.receivedDate !== undefined) { + updateData.receivedDate = req.body.receivedDate ? new Date(req.body.receivedDate) : null; + } + if (req.body.resolutionDate !== undefined) { + updateData.resolutionDate = req.body.resolutionDate ? new Date(req.body.resolutionDate) : null; + } + + const rma = await prisma.rMA.update({ + where: { id }, + data: updateData, + include: { + status: true, + proposedSolution: true, + customer: { select: { id: true, name: true } }, + assignedTo: { select: { id: true, name: true } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'RMA', id, updateData); + } + + successResponse(res, rma, 'Reklamácia bola aktualizovaná.'); + } catch (error) { + console.error('Error updating RMA:', error); + errorResponse(res, 'Chyba pri aktualizácii reklamácie.', 500); + } +}; + +export const deleteRMA = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const rma = await prisma.rMA.findUnique({ where: { id } }); + + if (!rma) { + errorResponse(res, 'Reklamácia nebola nájdená.', 404); + return; + } + + await prisma.rMA.delete({ where: { id } }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'RMA', id); + } + + successResponse(res, null, 'Reklamácia bola vymazaná.'); + } catch (error) { + console.error('Error deleting RMA:', error); + errorResponse(res, 'Chyba pri mazaní reklamácie.', 500); + } +}; + +export const updateRMAStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { statusId, notes } = req.body; + + const rma = await prisma.rMA.findUnique({ + where: { id }, + include: { status: true }, + }); + + if (!rma) { + errorResponse(res, 'Reklamácia nebola nájdená.', 404); + return; + } + + const newStatus = await prisma.rMAStatus.findUnique({ where: { id: statusId } }); + + if (!newStatus) { + errorResponse(res, 'Status nebol nájdený.', 404); + return; + } + + // Validate transition (basic - can be enhanced with workflow rules) + const currentStatus = rma.status; + if (currentStatus.canTransitionTo) { + const allowedTransitions = currentStatus.canTransitionTo as string[]; + if (!allowedTransitions.includes(newStatus.code)) { + errorResponse( + res, + `Prechod zo statusu "${currentStatus.name}" na "${newStatus.name}" nie je povolený.`, + 400 + ); + return; + } + } + + const updatedRMA = await prisma.rMA.update({ + where: { id }, + data: { + statusId, + closedAt: newStatus.isFinal ? new Date() : null, + }, + include: { status: true }, + }); + + // Create status history + await prisma.rMAStatusHistory.create({ + data: { + rmaId: id, + fromStatusId: rma.statusId, + toStatusId: statusId, + changedById: req.user!.userId, + notes, + }, + }); + + if (req.logActivity) { + await req.logActivity('STATUS_CHANGE', 'RMA', id, { + from: currentStatus.code, + to: newStatus.code, + }); + } + + successResponse(res, updatedRMA, 'Status reklamácie bol zmenený.'); + } catch (error) { + console.error('Error updating RMA status:', error); + errorResponse(res, 'Chyba pri zmene statusu.', 500); + } +}; + +export const approveRMA = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const rma = await prisma.rMA.update({ + where: { id }, + data: { + requiresApproval: false, + approvedById: req.user!.userId, + approvedAt: new Date(), + }, + include: { status: true }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'RMA', id, { approved: true }); + } + + successResponse(res, rma, 'Reklamácia bola schválená.'); + } catch (error) { + console.error('Error approving RMA:', error); + errorResponse(res, 'Chyba pri schvaľovaní reklamácie.', 500); + } +}; + +export const addRMAComment = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { content } = req.body; + + const comment = await prisma.rMAComment.create({ + data: { + rmaId: id, + userId: req.user!.userId, + content, + }, + include: { + user: { select: { id: true, name: true } }, + }, + }); + + successResponse(res, comment, 'Komentár bol pridaný.', 201); + } catch (error) { + console.error('Error adding RMA comment:', error); + errorResponse(res, 'Chyba pri pridávaní komentára.', 500); + } +}; + +export const generateRMANumberEndpoint = async (_req: AuthRequest, res: Response): Promise => { + try { + const rmaNumber = await generateRMANumber(); + successResponse(res, { rmaNumber }); + } catch (error) { + console.error('Error generating RMA number:', error); + errorResponse(res, 'Chyba pri generovaní RMA čísla.', 500); + } +}; diff --git a/backend/src/controllers/settings.controller.ts b/backend/src/controllers/settings.controller.ts new file mode 100644 index 0000000..14bf28f --- /dev/null +++ b/backend/src/controllers/settings.controller.ts @@ -0,0 +1,450 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { configService } from '../services/config.service'; + +// ==================== EQUIPMENT TYPES ==================== + +export const getEquipmentTypes = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const types = await configService.getEquipmentTypes(activeOnly); + successResponse(res, types); + } catch (error) { + console.error('Error fetching equipment types:', error); + errorResponse(res, 'Chyba pri načítaní typov zariadení.', 500); + } +}; + +export const createEquipmentType = async (req: AuthRequest, res: Response): Promise => { + try { + const type = await prisma.equipmentType.create({ data: req.body }); + configService.clearCacheKey('equipment_types'); + successResponse(res, type, 'Typ zariadenia bol vytvorený.', 201); + } catch (error) { + console.error('Error creating equipment type:', error); + errorResponse(res, 'Chyba pri vytváraní typu zariadenia.', 500); + } +}; + +export const updateEquipmentType = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const type = await prisma.equipmentType.update({ where: { id }, data: req.body }); + configService.clearCacheKey('equipment_types'); + successResponse(res, type, 'Typ zariadenia bol aktualizovaný.'); + } catch (error) { + console.error('Error updating equipment type:', error); + errorResponse(res, 'Chyba pri aktualizácii typu zariadenia.', 500); + } +}; + +export const deleteEquipmentType = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.equipmentType.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('equipment_types'); + successResponse(res, null, 'Typ zariadenia bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting equipment type:', error); + errorResponse(res, 'Chyba pri mazaní typu zariadenia.', 500); + } +}; + +// ==================== REVISION TYPES ==================== + +export const getRevisionTypes = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const types = await configService.getRevisionTypes(activeOnly); + successResponse(res, types); + } catch (error) { + console.error('Error fetching revision types:', error); + errorResponse(res, 'Chyba pri načítaní typov revízií.', 500); + } +}; + +export const createRevisionType = async (req: AuthRequest, res: Response): Promise => { + try { + const type = await prisma.revisionType.create({ data: req.body }); + configService.clearCacheKey('revision_types'); + successResponse(res, type, 'Typ revízie bol vytvorený.', 201); + } catch (error) { + console.error('Error creating revision type:', error); + errorResponse(res, 'Chyba pri vytváraní typu revízie.', 500); + } +}; + +export const updateRevisionType = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const type = await prisma.revisionType.update({ where: { id }, data: req.body }); + configService.clearCacheKey('revision_types'); + successResponse(res, type, 'Typ revízie bol aktualizovaný.'); + } catch (error) { + console.error('Error updating revision type:', error); + errorResponse(res, 'Chyba pri aktualizácii typu revízie.', 500); + } +}; + +export const deleteRevisionType = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.revisionType.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('revision_types'); + successResponse(res, null, 'Typ revízie bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting revision type:', error); + errorResponse(res, 'Chyba pri mazaní typu revízie.', 500); + } +}; + +// ==================== RMA STATUSES ==================== + +export const getRMAStatuses = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const statuses = await configService.getRMAStatuses(activeOnly); + successResponse(res, statuses); + } catch (error) { + console.error('Error fetching RMA statuses:', error); + errorResponse(res, 'Chyba pri načítaní statusov reklamácií.', 500); + } +}; + +export const createRMAStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const status = await prisma.rMAStatus.create({ data: req.body }); + configService.clearCacheKey('rma_statuses'); + successResponse(res, status, 'Status reklamácie bol vytvorený.', 201); + } catch (error) { + console.error('Error creating RMA status:', error); + errorResponse(res, 'Chyba pri vytváraní statusu reklamácie.', 500); + } +}; + +export const updateRMAStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const status = await prisma.rMAStatus.update({ where: { id }, data: req.body }); + configService.clearCacheKey('rma_statuses'); + successResponse(res, status, 'Status reklamácie bol aktualizovaný.'); + } catch (error) { + console.error('Error updating RMA status:', error); + errorResponse(res, 'Chyba pri aktualizácii statusu reklamácie.', 500); + } +}; + +export const deleteRMAStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.rMAStatus.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('rma_statuses'); + successResponse(res, null, 'Status reklamácie bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting RMA status:', error); + errorResponse(res, 'Chyba pri mazaní statusu reklamácie.', 500); + } +}; + +// ==================== RMA SOLUTIONS ==================== + +export const getRMASolutions = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const solutions = await configService.getRMASolutions(activeOnly); + successResponse(res, solutions); + } catch (error) { + console.error('Error fetching RMA solutions:', error); + errorResponse(res, 'Chyba pri načítaní riešení reklamácií.', 500); + } +}; + +export const createRMASolution = async (req: AuthRequest, res: Response): Promise => { + try { + const solution = await prisma.rMASolution.create({ data: req.body }); + configService.clearCacheKey('rma_solutions'); + successResponse(res, solution, 'Riešenie reklamácie bolo vytvorené.', 201); + } catch (error) { + console.error('Error creating RMA solution:', error); + errorResponse(res, 'Chyba pri vytváraní riešenia reklamácie.', 500); + } +}; + +export const updateRMASolution = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const solution = await prisma.rMASolution.update({ where: { id }, data: req.body }); + configService.clearCacheKey('rma_solutions'); + successResponse(res, solution, 'Riešenie reklamácie bolo aktualizované.'); + } catch (error) { + console.error('Error updating RMA solution:', error); + errorResponse(res, 'Chyba pri aktualizácii riešenia reklamácie.', 500); + } +}; + +export const deleteRMASolution = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.rMASolution.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('rma_solutions'); + successResponse(res, null, 'Riešenie reklamácie bolo deaktivované.'); + } catch (error) { + console.error('Error deleting RMA solution:', error); + errorResponse(res, 'Chyba pri mazaní riešenia reklamácie.', 500); + } +}; + +// ==================== TASK STATUSES ==================== + +export const getTaskStatuses = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const statuses = await configService.getTaskStatuses(activeOnly); + successResponse(res, statuses); + } catch (error) { + console.error('Error fetching task statuses:', error); + errorResponse(res, 'Chyba pri načítaní statusov úloh.', 500); + } +}; + +export const createTaskStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const status = await prisma.taskStatus.create({ data: req.body }); + configService.clearCacheKey('task_statuses'); + successResponse(res, status, 'Status úlohy bol vytvorený.', 201); + } catch (error) { + console.error('Error creating task status:', error); + errorResponse(res, 'Chyba pri vytváraní statusu úlohy.', 500); + } +}; + +export const updateTaskStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const status = await prisma.taskStatus.update({ where: { id }, data: req.body }); + configService.clearCacheKey('task_statuses'); + successResponse(res, status, 'Status úlohy bol aktualizovaný.'); + } catch (error) { + console.error('Error updating task status:', error); + errorResponse(res, 'Chyba pri aktualizácii statusu úlohy.', 500); + } +}; + +export const deleteTaskStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.taskStatus.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('task_statuses'); + successResponse(res, null, 'Status úlohy bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting task status:', error); + errorResponse(res, 'Chyba pri mazaní statusu úlohy.', 500); + } +}; + +// ==================== PRIORITIES ==================== + +export const getPriorities = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const priorities = await configService.getPriorities(activeOnly); + successResponse(res, priorities); + } catch (error) { + console.error('Error fetching priorities:', error); + errorResponse(res, 'Chyba pri načítaní priorít.', 500); + } +}; + +export const createPriority = async (req: AuthRequest, res: Response): Promise => { + try { + const priority = await prisma.priority.create({ data: req.body }); + configService.clearCacheKey('priorities'); + successResponse(res, priority, 'Priorita bola vytvorená.', 201); + } catch (error) { + console.error('Error creating priority:', error); + errorResponse(res, 'Chyba pri vytváraní priority.', 500); + } +}; + +export const updatePriority = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const priority = await prisma.priority.update({ where: { id }, data: req.body }); + configService.clearCacheKey('priorities'); + successResponse(res, priority, 'Priorita bola aktualizovaná.'); + } catch (error) { + console.error('Error updating priority:', error); + errorResponse(res, 'Chyba pri aktualizácii priority.', 500); + } +}; + +export const deletePriority = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.priority.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('priorities'); + successResponse(res, null, 'Priorita bola deaktivovaná.'); + } catch (error) { + console.error('Error deleting priority:', error); + errorResponse(res, 'Chyba pri mazaní priority.', 500); + } +}; + +// ==================== TAGS ==================== + +export const getTags = async (req: AuthRequest, res: Response): Promise => { + try { + const entityType = getQueryString(req, 'entityType'); + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const tags = await configService.getTags(entityType, activeOnly); + successResponse(res, tags); + } catch (error) { + console.error('Error fetching tags:', error); + errorResponse(res, 'Chyba pri načítaní tagov.', 500); + } +}; + +export const createTag = async (req: AuthRequest, res: Response): Promise => { + try { + const tag = await prisma.tag.create({ data: req.body }); + configService.clearCacheKey('tags'); + successResponse(res, tag, 'Tag bol vytvorený.', 201); + } catch (error) { + console.error('Error creating tag:', error); + errorResponse(res, 'Chyba pri vytváraní tagu.', 500); + } +}; + +export const updateTag = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const tag = await prisma.tag.update({ where: { id }, data: req.body }); + configService.clearCacheKey('tags'); + successResponse(res, tag, 'Tag bol aktualizovaný.'); + } catch (error) { + console.error('Error updating tag:', error); + errorResponse(res, 'Chyba pri aktualizácii tagu.', 500); + } +}; + +export const deleteTag = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.tag.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('tags'); + successResponse(res, null, 'Tag bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting tag:', error); + errorResponse(res, 'Chyba pri mazaní tagu.', 500); + } +}; + +// ==================== USER ROLES ==================== + +export const getUserRoles = async (req: AuthRequest, res: Response): Promise => { + try { + const activeOnly = getQueryString(req, 'activeOnly') !== 'false'; + const roles = await configService.getUserRoles(activeOnly); + successResponse(res, roles); + } catch (error) { + console.error('Error fetching user roles:', error); + errorResponse(res, 'Chyba pri načítaní rolí.', 500); + } +}; + +export const createUserRole = async (req: AuthRequest, res: Response): Promise => { + try { + const role = await prisma.userRole.create({ data: req.body }); + configService.clearCacheKey('user_roles'); + successResponse(res, role, 'Rola bola vytvorená.', 201); + } catch (error) { + console.error('Error creating user role:', error); + errorResponse(res, 'Chyba pri vytváraní roly.', 500); + } +}; + +export const updateUserRole = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const role = await prisma.userRole.update({ where: { id }, data: req.body }); + configService.clearCacheKey('user_roles'); + successResponse(res, role, 'Rola bola aktualizovaná.'); + } catch (error) { + console.error('Error updating user role:', error); + errorResponse(res, 'Chyba pri aktualizácii roly.', 500); + } +}; + +export const deleteUserRole = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + await prisma.userRole.update({ where: { id }, data: { active: false } }); + configService.clearCacheKey('user_roles'); + successResponse(res, null, 'Rola bola deaktivovaná.'); + } catch (error) { + console.error('Error deleting user role:', error); + errorResponse(res, 'Chyba pri mazaní roly.', 500); + } +}; + +// ==================== SYSTEM SETTINGS ==================== + +export const getSystemSettings = async (_req: AuthRequest, res: Response): Promise => { + try { + const settings = await prisma.systemSetting.findMany({ + orderBy: [{ category: 'asc' }, { key: 'asc' }], + }); + successResponse(res, settings); + } catch (error) { + console.error('Error fetching system settings:', error); + errorResponse(res, 'Chyba pri načítaní nastavení.', 500); + } +}; + +export const getSystemSetting = async (req: AuthRequest, res: Response): Promise => { + try { + const key = getParam(req, 'key'); + const setting = await prisma.systemSetting.findUnique({ where: { key } }); + + if (!setting) { + errorResponse(res, 'Nastavenie nebolo nájdené.', 404); + return; + } + + successResponse(res, setting); + } catch (error) { + console.error('Error fetching system setting:', error); + errorResponse(res, 'Chyba pri načítaní nastavenia.', 500); + } +}; + +export const updateSystemSetting = async (req: AuthRequest, res: Response): Promise => { + try { + const key = getParam(req, 'key'); + const { value } = req.body; + + const setting = await prisma.systemSetting.update({ + where: { key }, + data: { value }, + }); + + configService.clearCacheKey(`setting_${key}`); + successResponse(res, setting, 'Nastavenie bolo aktualizované.'); + } catch (error) { + console.error('Error updating system setting:', error); + errorResponse(res, 'Chyba pri aktualizácii nastavenia.', 500); + } +}; + +export const getSystemSettingsByCategory = async (req: AuthRequest, res: Response): Promise => { + try { + const category = getParam(req, 'category'); + const settings = await configService.getSettingsByCategory(category); + successResponse(res, settings); + } catch (error) { + console.error('Error fetching system settings by category:', error); + errorResponse(res, 'Chyba pri načítaní nastavení.', 500); + } +}; diff --git a/backend/src/controllers/tasks.controller.ts b/backend/src/controllers/tasks.controller.ts new file mode 100644 index 0000000..c696bc0 --- /dev/null +++ b/backend/src/controllers/tasks.controller.ts @@ -0,0 +1,400 @@ +import { Response } from 'express'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { configService } from '../services/config.service'; + +export const getTasks = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const projectId = getQueryString(req, 'projectId'); + const statusId = getQueryString(req, 'statusId'); + const priorityId = getQueryString(req, 'priorityId'); + const createdById = getQueryString(req, 'createdById'); + const assigneeId = getQueryString(req, 'assigneeId'); + + const where = { + ...(projectId && { projectId }), + ...(statusId && { statusId }), + ...(priorityId && { priorityId }), + ...(createdById && { createdById }), + ...(assigneeId && { + assignees: { some: { userId: assigneeId } }, + }), + ...(search && { + OR: [ + { title: { contains: search, mode: 'insensitive' as const } }, + { description: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [tasks, total] = await Promise.all([ + prisma.task.findMany({ + where, + skip, + take: limit, + orderBy: [{ priority: { level: 'desc' } }, { createdAt: 'desc' }], + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + assignees: { + include: { user: { select: { id: true, name: true } } }, + }, + _count: { select: { subTasks: true, comments: true } }, + }, + }), + prisma.task.count({ where }), + ]); + + paginatedResponse(res, tasks, total, page, limit); + } catch (error) { + console.error('Error fetching tasks:', error); + errorResponse(res, 'Chyba pri načítaní úloh.', 500); + } +}; + +export const getTask = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const task = await prisma.task.findUnique({ + where: { id }, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + parent: { select: { id: true, title: true } }, + subTasks: { + include: { + status: true, + assignees: { include: { user: { select: { id: true, name: true } } } }, + }, + }, + createdBy: { select: { id: true, name: true, email: true } }, + assignees: { + include: { user: { select: { id: true, name: true, email: true } } }, + }, + comments: { + orderBy: { createdAt: 'desc' }, + take: 10, + }, + tags: { include: { tag: true } }, + }, + }); + + if (!task) { + errorResponse(res, 'Úloha nebola nájdená.', 404); + return; + } + + successResponse(res, task); + } catch (error) { + console.error('Error fetching task:', error); + errorResponse(res, 'Chyba pri načítaní úlohy.', 500); + } +}; + +export const createTask = async (req: AuthRequest, res: Response): Promise => { + try { + let { statusId, priorityId } = req.body; + + // Get defaults if not provided + if (!statusId) { + const initialStatus = await configService.getInitialTaskStatus(); + if (initialStatus) { + statusId = initialStatus.id; + } else { + // Fallback: vezmi prvý aktívny status + const allStatuses = await configService.getTaskStatuses(); + const statuses = allStatuses as { id: string }[]; + if (statuses.length > 0) { + statusId = statuses[0].id; + } else { + errorResponse(res, 'Žiadny status nie je nakonfigurovaný. Spustite seed.', 500); + return; + } + } + } + + if (!priorityId) { + const defaultPriority = await configService.getDefaultPriority(); + if (defaultPriority) { + priorityId = defaultPriority.id; + } else { + // Fallback: vezmi prvú aktívnu prioritu + const allPriorities = await configService.getPriorities(); + const priorities = allPriorities as { id: string }[]; + if (priorities.length > 0) { + priorityId = priorities[0].id; + } else { + errorResponse(res, 'Žiadna priorita nie je nakonfigurovaná. Spustite seed.', 500); + return; + } + } + } + + const task = await prisma.task.create({ + data: { + title: req.body.title, + description: req.body.description, + projectId: req.body.projectId || null, + parentId: req.body.parentId || null, + statusId, + priorityId, + deadline: req.body.deadline ? new Date(req.body.deadline) : null, + createdById: req.user!.userId, + }, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + createdBy: { select: { id: true, name: true } }, + }, + }); + + // Add assignees if provided + if (req.body.assigneeIds && req.body.assigneeIds.length > 0) { + await prisma.taskAssignee.createMany({ + data: req.body.assigneeIds.map((userId: string) => ({ + taskId: task.id, + userId, + })), + }); + } + + if (req.logActivity) { + await req.logActivity('CREATE', 'Task', task.id, { title: task.title }); + } + + successResponse(res, task, 'Úloha bola vytvorená.', 201); + } catch (error) { + console.error('Error creating task:', error); + errorResponse(res, 'Chyba pri vytváraní úlohy.', 500); + } +}; + +export const updateTask = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const existing = await prisma.task.findUnique({ where: { id } }); + + if (!existing) { + errorResponse(res, 'Úloha nebola nájdená.', 404); + return; + } + + const updateData: Record = {}; + if (req.body.title) updateData.title = req.body.title; + if (req.body.description !== undefined) updateData.description = req.body.description; + if (req.body.projectId !== undefined) updateData.projectId = req.body.projectId || null; + if (req.body.parentId !== undefined) updateData.parentId = req.body.parentId || null; + if (req.body.statusId) updateData.statusId = req.body.statusId; + if (req.body.priorityId) updateData.priorityId = req.body.priorityId; + if (req.body.deadline !== undefined) { + updateData.deadline = req.body.deadline ? new Date(req.body.deadline) : null; + } + + const task = await prisma.task.update({ + where: { id }, + data: updateData, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + assignees: { include: { user: { select: { id: true, name: true } } } }, + }, + }); + + // Update assignees if provided + if (req.body.assigneeIds !== undefined) { + // Remove all current assignees + await prisma.taskAssignee.deleteMany({ where: { taskId: id } }); + + // Add new assignees + if (req.body.assigneeIds.length > 0) { + await prisma.taskAssignee.createMany({ + data: req.body.assigneeIds.map((userId: string) => ({ + taskId: id, + userId, + })), + }); + } + } + + // Re-fetch task with updated assignees + const updatedTask = await prisma.task.findUnique({ + where: { id }, + include: { + status: true, + priority: true, + project: { select: { id: true, name: true } }, + assignees: { include: { user: { select: { id: true, name: true } } } }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'Task', id, updateData); + } + + successResponse(res, updatedTask, 'Úloha bola aktualizovaná.'); + } catch (error) { + console.error('Error updating task:', error); + errorResponse(res, 'Chyba pri aktualizácii úlohy.', 500); + } +}; + +export const deleteTask = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const task = await prisma.task.findUnique({ where: { id } }); + + if (!task) { + errorResponse(res, 'Úloha nebola nájdená.', 404); + return; + } + + await prisma.task.delete({ where: { id } }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'Task', id); + } + + successResponse(res, null, 'Úloha bola vymazaná.'); + } catch (error) { + console.error('Error deleting task:', error); + errorResponse(res, 'Chyba pri mazaní úlohy.', 500); + } +}; + +export const updateTaskStatus = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { statusId } = req.body; + + const status = await prisma.taskStatus.findUnique({ where: { id: statusId } }); + + const task = await prisma.task.update({ + where: { id }, + data: { + statusId, + completedAt: status?.isFinal ? new Date() : null, + }, + include: { status: true }, + }); + + if (req.logActivity) { + await req.logActivity('STATUS_CHANGE', 'Task', id, { statusId }); + } + + successResponse(res, task, 'Status úlohy bol zmenený.'); + } catch (error) { + console.error('Error updating task status:', error); + errorResponse(res, 'Chyba pri zmene statusu.', 500); + } +}; + +export const addTaskAssignee = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { userId } = req.body; + + const assignee = await prisma.taskAssignee.create({ + data: { taskId: id, userId }, + include: { user: { select: { id: true, name: true, email: true } } }, + }); + + successResponse(res, assignee, 'Priraďovateľ bol pridaný.', 201); + } catch (error) { + console.error('Error adding task assignee:', error); + errorResponse(res, 'Chyba pri pridávaní priraďovateľa.', 500); + } +}; + +export const removeTaskAssignee = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const userId = getParam(req, 'userId'); + + await prisma.taskAssignee.delete({ + where: { taskId_userId: { taskId: id, userId } }, + }); + + successResponse(res, null, 'Priraďovateľ bol odstránený.'); + } catch (error) { + console.error('Error removing task assignee:', error); + errorResponse(res, 'Chyba pri odstraňovaní priraďovateľa.', 500); + } +}; + +export const getTaskComments = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const comments = await prisma.comment.findMany({ + where: { taskId: id }, + orderBy: { createdAt: 'desc' }, + include: { + user: { select: { id: true, name: true } }, + }, + }); + + successResponse(res, comments); + } catch (error) { + console.error('Error fetching task comments:', error); + errorResponse(res, 'Chyba pri načítaní komentárov.', 500); + } +}; + +export const addTaskComment = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { content } = req.body; + const userId = req.user!.userId; + + // Načítať úlohu s assignees pre kontrolu oprávnení + const task = await prisma.task.findUnique({ + where: { id }, + include: { + assignees: { select: { userId: true } }, + }, + }); + + if (!task) { + errorResponse(res, 'Úloha nebola nájdená.', 404); + return; + } + + // Kontrola oprávnení - len autor alebo priradený môže komentovať + const isCreator = task.createdById === userId; + const isAssignee = task.assignees.some(a => a.userId === userId); + + if (!isCreator && !isAssignee) { + errorResponse(res, 'Nemáte oprávnenie komentovať túto úlohu.', 403); + return; + } + + const comment = await prisma.comment.create({ + data: { + taskId: id, + userId, + content, + }, + include: { + user: { select: { id: true, name: true } }, + }, + }); + + successResponse(res, comment, 'Komentár bol pridaný.', 201); + } catch (error) { + console.error('Error adding task comment:', error); + errorResponse(res, 'Chyba pri pridávaní komentára.', 500); + } +}; diff --git a/backend/src/controllers/users.controller.ts b/backend/src/controllers/users.controller.ts new file mode 100644 index 0000000..515e85d --- /dev/null +++ b/backend/src/controllers/users.controller.ts @@ -0,0 +1,272 @@ +import { Response } from 'express'; +import bcrypt from 'bcryptjs'; +import prisma from '../config/database'; +import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; +import { AuthRequest } from '../middleware/auth.middleware'; + +// Jednoduchý zoznam aktívnych používateľov (pre select/dropdown) +// Podporuje server-side vyhľadávanie pre lepší výkon pri veľkom počte používateľov +export const getUsersSimple = async (req: AuthRequest, res: Response): Promise => { + try { + const search = getQueryString(req, 'search'); + const limit = parseQueryInt(req.query.limit, 50); // Default 50, max 100 + const actualLimit = Math.min(limit, 100); + + const where = { + active: true, + ...(search && { + OR: [ + { name: { contains: search, mode: 'insensitive' as const } }, + { email: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const users = await prisma.user.findMany({ + where, + take: actualLimit, + orderBy: { name: 'asc' }, + select: { + id: true, + name: true, + email: true, + }, + }); + + successResponse(res, users); + } catch (error) { + console.error('Error fetching users simple:', error); + errorResponse(res, 'Chyba pri načítaní používateľov.', 500); + } +}; + +export const getUsers = async (req: AuthRequest, res: Response): Promise => { + try { + const page = parseQueryInt(req.query.page, 1); + const limit = parseQueryInt(req.query.limit, 20); + const skip = (page - 1) * limit; + const search = getQueryString(req, 'search'); + const active = getQueryString(req, 'active'); + const roleId = getQueryString(req, 'roleId'); + + const where = { + ...(active !== undefined && { active: active === 'true' }), + ...(roleId && { roleId }), + ...(search && { + OR: [ + { name: { contains: search, mode: 'insensitive' as const } }, + { email: { contains: search, mode: 'insensitive' as const } }, + ], + }), + }; + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + email: true, + name: true, + active: true, + createdAt: true, + updatedAt: true, + role: { + select: { + id: true, + code: true, + name: true, + }, + }, + }, + }), + prisma.user.count({ where }), + ]); + + paginatedResponse(res, users, total, page, limit); + } catch (error) { + console.error('Error fetching users:', error); + errorResponse(res, 'Chyba pri načítaní používateľov.', 500); + } +}; + +export const getUser = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + const user = await prisma.user.findUnique({ + where: { id }, + select: { + id: true, + email: true, + name: true, + active: true, + createdAt: true, + updatedAt: true, + role: { + select: { + id: true, + code: true, + name: true, + permissions: true, + }, + }, + }, + }); + + if (!user) { + errorResponse(res, 'Používateľ nebol nájdený.', 404); + return; + } + + successResponse(res, user); + } catch (error) { + console.error('Error fetching user:', error); + errorResponse(res, 'Chyba pri načítaní používateľa.', 500); + } +}; + +export const updateUser = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { email, name, active, password } = req.body; + + const existingUser = await prisma.user.findUnique({ where: { id } }); + + if (!existingUser) { + errorResponse(res, 'Používateľ nebol nájdený.', 404); + return; + } + + // Check email uniqueness if changing + if (email && email !== existingUser.email) { + const emailExists = await prisma.user.findUnique({ where: { email } }); + if (emailExists) { + errorResponse(res, 'Email je už používaný.', 409); + return; + } + } + + const updateData: Record = {}; + if (email) updateData.email = email; + if (name) updateData.name = name; + if (active !== undefined) updateData.active = active; + if (password) updateData.password = await bcrypt.hash(password, 10); + + const user = await prisma.user.update({ + where: { id }, + data: updateData, + select: { + id: true, + email: true, + name: true, + active: true, + role: { + select: { + id: true, + code: true, + name: true, + }, + }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'User', id, updateData); + } + + successResponse(res, user, 'Používateľ bol aktualizovaný.'); + } catch (error) { + console.error('Error updating user:', error); + errorResponse(res, 'Chyba pri aktualizácii používateľa.', 500); + } +}; + +export const deleteUser = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + + // Prevent self-deletion + if (req.user?.userId === id) { + errorResponse(res, 'Nemôžete vymazať vlastný účet.', 400); + return; + } + + const user = await prisma.user.findUnique({ where: { id } }); + + if (!user) { + errorResponse(res, 'Používateľ nebol nájdený.', 404); + return; + } + + // Soft delete - just deactivate + await prisma.user.update({ + where: { id }, + data: { active: false }, + }); + + if (req.logActivity) { + await req.logActivity('DELETE', 'User', id); + } + + successResponse(res, null, 'Používateľ bol deaktivovaný.'); + } catch (error) { + console.error('Error deleting user:', error); + errorResponse(res, 'Chyba pri mazaní používateľa.', 500); + } +}; + +export const updateUserRole = async (req: AuthRequest, res: Response): Promise => { + try { + const id = getParam(req, 'id'); + const { roleId } = req.body; + + // Prevent changing own role + if (req.user?.userId === id) { + errorResponse(res, 'Nemôžete zmeniť vlastnú rolu.', 400); + return; + } + + const user = await prisma.user.findUnique({ where: { id } }); + + if (!user) { + errorResponse(res, 'Používateľ nebol nájdený.', 404); + return; + } + + const role = await prisma.userRole.findUnique({ where: { id: roleId } }); + + if (!role) { + errorResponse(res, 'Rola neexistuje.', 404); + return; + } + + const updatedUser = await prisma.user.update({ + where: { id }, + data: { roleId }, + select: { + id: true, + email: true, + name: true, + role: { + select: { + id: true, + code: true, + name: true, + }, + }, + }, + }); + + if (req.logActivity) { + await req.logActivity('UPDATE', 'User', id, { roleId, roleName: role.name }); + } + + successResponse(res, updatedUser, 'Rola používateľa bola zmenená.'); + } catch (error) { + console.error('Error updating user role:', error); + errorResponse(res, 'Chyba pri zmene roly.', 500); + } +}; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..5a16866 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,72 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import { env } from './config/env'; +import routes from './routes'; +import { errorHandler, notFoundHandler } from './middleware/errorHandler'; +import prisma from './config/database'; + +const app = express(); + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: env.FRONTEND_URL, + credentials: true, +})); + +// Request parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Logging +if (env.isDev) { + app.use(morgan('dev')); +} else { + app.use(morgan('combined')); +} + +// Health check +app.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// API routes +app.use('/api', routes); + +// Error handlers +app.use(notFoundHandler); +app.use(errorHandler); + +// Start server +const startServer = async () => { + try { + // Test database connection + await prisma.$connect(); + console.log('Database connected successfully'); + + app.listen(env.PORT, () => { + console.log(`Server running on http://localhost:${env.PORT}`); + console.log(`Environment: ${env.NODE_ENV}`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +}; + +// Handle graceful shutdown +process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down...'); + await prisma.$disconnect(); + process.exit(0); +}); + +process.on('SIGINT', async () => { + console.log('SIGINT received, shutting down...'); + await prisma.$disconnect(); + process.exit(0); +}); + +startServer(); diff --git a/backend/src/middleware/activityLog.middleware.ts b/backend/src/middleware/activityLog.middleware.ts new file mode 100644 index 0000000..a766099 --- /dev/null +++ b/backend/src/middleware/activityLog.middleware.ts @@ -0,0 +1,73 @@ +import { Response, NextFunction } from 'express'; +import { AuthRequest } from './auth.middleware'; +import prisma from '../config/database'; +import { Prisma } from '@prisma/client'; + +type ActionType = 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE' | 'LOGIN' | 'LOGOUT'; +type EntityType = 'User' | 'Project' | 'Task' | 'Customer' | 'Equipment' | 'Revision' | 'RMA'; + +export const logActivity = async ( + userId: string, + action: ActionType, + entity: EntityType, + entityId: string, + changes?: Record, + ipAddress?: string, + userAgent?: string +): Promise => { + try { + await prisma.activityLog.create({ + data: { + userId, + action, + entity, + entityId, + changes: changes as Prisma.InputJsonValue, + ipAddress, + userAgent, + }, + }); + } catch (error) { + console.error('Failed to log activity:', error); + } +}; + +// Middleware to attach logging helper to request +export const activityLogger = ( + req: AuthRequest, + _res: Response, + next: NextFunction +): void => { + req.logActivity = async ( + action: ActionType, + entity: EntityType, + entityId: string, + changes?: Record + ) => { + if (req.user) { + await logActivity( + req.user.userId, + action, + entity, + entityId, + changes, + req.ip, + req.get('User-Agent') + ); + } + }; + + next(); +}; + +// Extend Express Request type +declare module 'express' { + interface Request { + logActivity?: ( + action: ActionType, + entity: EntityType, + entityId: string, + changes?: Record + ) => Promise; + } +} diff --git a/backend/src/middleware/auth.middleware.ts b/backend/src/middleware/auth.middleware.ts new file mode 100644 index 0000000..7cc1e8b --- /dev/null +++ b/backend/src/middleware/auth.middleware.ts @@ -0,0 +1,100 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyAccessToken, TokenPayload } from '../config/jwt'; +import { errorResponse } from '../utils/helpers'; +import prisma from '../config/database'; + +export interface AuthRequest extends Request { + user?: TokenPayload & { + permissions: Record; + }; +} + +export const authenticate = async ( + req: AuthRequest, + res: Response, + next: NextFunction +): Promise => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + errorResponse(res, 'Prístup zamietnutý. Token nebol poskytnutý.', 401); + return; + } + + const token = authHeader.split(' ')[1]; + + try { + const decoded = verifyAccessToken(token); + + // Get user role with permissions + const userRole = await prisma.userRole.findUnique({ + where: { id: decoded.roleId }, + }); + + if (!userRole || !userRole.active) { + errorResponse(res, 'Rola používateľa nie je aktívna.', 403); + return; + } + + // Check if user still exists and is active + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + }); + + if (!user || !user.active) { + errorResponse(res, 'Používateľ neexistuje alebo je neaktívny.', 403); + return; + } + + req.user = { + ...decoded, + permissions: userRole.permissions as Record, + }; + + next(); + } catch { + errorResponse(res, 'Neplatný alebo expirovaný token.', 401); + } + } catch (error) { + console.error('Auth middleware error:', error); + errorResponse(res, 'Chyba autentifikácie.', 500); + } +}; + +export const optionalAuth = async ( + req: AuthRequest, + res: Response, + next: NextFunction +): Promise => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + next(); + return; + } + + const token = authHeader.split(' ')[1]; + + try { + const decoded = verifyAccessToken(token); + const userRole = await prisma.userRole.findUnique({ + where: { id: decoded.roleId }, + }); + + if (userRole && userRole.active) { + req.user = { + ...decoded, + permissions: userRole.permissions as Record, + }; + } + } catch { + // Token invalid, continue without user + } + + next(); + } catch { + next(); + } +}; diff --git a/backend/src/middleware/errorHandler.ts b/backend/src/middleware/errorHandler.ts new file mode 100644 index 0000000..427a4f0 --- /dev/null +++ b/backend/src/middleware/errorHandler.ts @@ -0,0 +1,49 @@ +import { Request, Response, NextFunction } from 'express'; +import { env } from '../config/env'; + +export interface AppError extends Error { + statusCode?: number; + isOperational?: boolean; +} + +export const errorHandler = ( + err: AppError, + req: Request, + res: Response, + _next: NextFunction +): void => { + const statusCode = err.statusCode || 500; + const message = err.message || 'Interná chyba servera'; + + console.error(`[ERROR] ${statusCode} - ${message}`); + console.error(err.stack); + + res.status(statusCode).json({ + success: false, + message, + ...(env.isDev && { + stack: err.stack, + error: err, + }), + }); +}; + +export const notFoundHandler = (req: Request, res: Response): void => { + res.status(404).json({ + success: false, + message: `Route ${req.method} ${req.path} nenájdená`, + }); +}; + +export class ApiError extends Error { + statusCode: number; + isOperational: boolean; + + constructor(message: string, statusCode = 400) { + super(message); + this.statusCode = statusCode; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/backend/src/middleware/rbac.middleware.ts b/backend/src/middleware/rbac.middleware.ts new file mode 100644 index 0000000..accab9b --- /dev/null +++ b/backend/src/middleware/rbac.middleware.ts @@ -0,0 +1,115 @@ +import { Response, NextFunction } from 'express'; +import { AuthRequest } from './auth.middleware'; +import { errorResponse } from '../utils/helpers'; + +type Resource = 'projects' | 'tasks' | 'equipment' | 'rma' | 'customers' | 'settings' | 'users' | 'logs'; +type Action = 'create' | 'read' | 'update' | 'delete' | 'all' | 'approve' | '*'; + +export const hasPermission = ( + permissions: Record, + resource: Resource, + action: Action +): boolean => { + const resourcePermissions = permissions[resource]; + + if (!resourcePermissions) { + return false; + } + + // Check for wildcard permission + if (resourcePermissions.includes('*')) { + return true; + } + + // Check for 'all' permission (grants read/create/update/delete) + if (resourcePermissions.includes('all') && ['create', 'read', 'update', 'delete'].includes(action)) { + return true; + } + + // Check for specific permission + return resourcePermissions.includes(action); +}; + +export const checkPermission = (resource: Resource, action: Action) => { + return (req: AuthRequest, res: Response, next: NextFunction): void => { + if (!req.user) { + errorResponse(res, 'Nie ste prihlásený.', 401); + return; + } + + const { permissions } = req.user; + + if (!hasPermission(permissions, resource, action)) { + errorResponse(res, 'Nemáte oprávnenie na túto akciu.', 403); + return; + } + + next(); + }; +}; + +// Shorthand middleware factories +export const canCreate = (resource: Resource) => checkPermission(resource, 'create'); +export const canRead = (resource: Resource) => checkPermission(resource, 'read'); +export const canUpdate = (resource: Resource) => checkPermission(resource, 'update'); +export const canDelete = (resource: Resource) => checkPermission(resource, 'delete'); +export const canManage = (resource: Resource) => checkPermission(resource, 'all'); + +// Check if user has ROOT role +export const isRoot = (req: AuthRequest, res: Response, next: NextFunction): void => { + if (!req.user) { + errorResponse(res, 'Nie ste prihlásený.', 401); + return; + } + + if (req.user.roleCode !== 'ROOT') { + errorResponse(res, 'Táto akcia vyžaduje ROOT oprávnenia.', 403); + return; + } + + next(); +}; + +// Check if user has ADMIN or ROOT role +export const isAdmin = (req: AuthRequest, res: Response, next: NextFunction): void => { + if (!req.user) { + errorResponse(res, 'Nie ste prihlásený.', 401); + return; + } + + if (!['ROOT', 'ADMIN'].includes(req.user.roleCode)) { + errorResponse(res, 'Táto akcia vyžaduje ADMIN oprávnenia.', 403); + return; + } + + next(); +}; + +// Check ownership or admin permission +export const isOwnerOrAdmin = (getOwnerId: (req: AuthRequest) => Promise) => { + return async (req: AuthRequest, res: Response, next: NextFunction): Promise => { + if (!req.user) { + errorResponse(res, 'Nie ste prihlásený.', 401); + return; + } + + // Admins and ROOT can access anything + if (['ROOT', 'ADMIN'].includes(req.user.roleCode)) { + next(); + return; + } + + try { + const ownerId = await getOwnerId(req); + + if (ownerId === req.user.userId) { + next(); + return; + } + + errorResponse(res, 'Nemáte oprávnenie na túto akciu.', 403); + } catch { + errorResponse(res, 'Chyba pri overovaní oprávnení.', 500); + } + }; +}; diff --git a/backend/src/middleware/validate.middleware.ts b/backend/src/middleware/validate.middleware.ts new file mode 100644 index 0000000..9d7848c --- /dev/null +++ b/backend/src/middleware/validate.middleware.ts @@ -0,0 +1,71 @@ +import { Request, Response, NextFunction } from 'express'; +import { ZodSchema } from 'zod'; +import { errorResponse } from '../utils/helpers'; + +export const validate = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + schema.parse(req.body); + next(); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'issues' in error) { + const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> }; + const formattedErrors = zodError.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })); + + errorResponse(res, 'Validácia zlyhala.', 400, formattedErrors); + return; + } + + errorResponse(res, 'Neočakávaná chyba validácie.', 500); + } + }; +}; + +export const validateQuery = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + const result = schema.parse(req.query); + req.query = result as typeof req.query; + next(); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'issues' in error) { + const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> }; + const formattedErrors = zodError.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })); + + errorResponse(res, 'Neplatné query parametre.', 400, formattedErrors); + return; + } + + errorResponse(res, 'Neočakávaná chyba validácie.', 500); + } + }; +}; + +export const validateParams = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + const result = schema.parse(req.params); + req.params = result as typeof req.params; + next(); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'issues' in error) { + const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> }; + const formattedErrors = zodError.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })); + + errorResponse(res, 'Neplatné URL parametre.', 400, formattedErrors); + return; + } + + errorResponse(res, 'Neočakávaná chyba validácie.', 500); + } + }; +}; diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts new file mode 100644 index 0000000..afff083 --- /dev/null +++ b/backend/src/routes/auth.routes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import * as authController from '../controllers/auth.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { loginSchema, registerSchema } from '../utils/validators'; + +const router = Router(); + +router.post('/register', validate(registerSchema), authController.register); +router.post('/login', validate(loginSchema), authController.login); +router.post('/refresh', authController.refresh); +router.post('/logout', authenticate, authController.logout); +router.get('/me', authenticate, authController.getMe); + +export default router; diff --git a/backend/src/routes/customers.routes.ts b/backend/src/routes/customers.routes.ts new file mode 100644 index 0000000..e31511b --- /dev/null +++ b/backend/src/routes/customers.routes.ts @@ -0,0 +1,25 @@ +import { Router } from 'express'; +import * as customersController from '../controllers/customers.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { customerSchema } from '../utils/validators'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +router.get('/', canRead('customers'), customersController.getCustomers); +router.post('/', canCreate('customers'), validate(customerSchema), customersController.createCustomer); +router.get('/:id', canRead('customers'), customersController.getCustomer); +router.put('/:id', canUpdate('customers'), validate(customerSchema), customersController.updateCustomer); +router.delete('/:id', canDelete('customers'), customersController.deleteCustomer); + +// Customer relations +router.get('/:id/projects', canRead('customers'), customersController.getCustomerProjects); +router.get('/:id/equipment', canRead('customers'), customersController.getCustomerEquipment); +router.get('/:id/rmas', canRead('customers'), customersController.getCustomerRMAs); + +export default router; diff --git a/backend/src/routes/dashboard.routes.ts b/backend/src/routes/dashboard.routes.ts new file mode 100644 index 0000000..b9ad107 --- /dev/null +++ b/backend/src/routes/dashboard.routes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import * as dashboardController from '../controllers/dashboard.controller'; +import { authenticate } from '../middleware/auth.middleware'; + +const router = Router(); + +router.use(authenticate); + +router.get('/', dashboardController.getDashboard); +router.get('/today', dashboardController.getDashboardToday); +router.get('/week', dashboardController.getDashboardWeek); +router.get('/stats', dashboardController.getDashboardStats); +router.get('/reminders', dashboardController.getDashboardReminders); + +export default router; diff --git a/backend/src/routes/equipment.routes.ts b/backend/src/routes/equipment.routes.ts new file mode 100644 index 0000000..77b1703 --- /dev/null +++ b/backend/src/routes/equipment.routes.ts @@ -0,0 +1,25 @@ +import { Router } from 'express'; +import * as equipmentController from '../controllers/equipment.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { equipmentSchema } from '../utils/validators'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +router.get('/', canRead('equipment'), equipmentController.getEquipment); +router.post('/', canCreate('equipment'), validate(equipmentSchema), equipmentController.createEquipment); +router.get('/reminders', canRead('equipment'), equipmentController.getEquipmentReminders); +router.get('/:id', canRead('equipment'), equipmentController.getEquipmentById); +router.put('/:id', canUpdate('equipment'), equipmentController.updateEquipment); +router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipment); + +// Revisions +router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions); +router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision); + +export default router; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts new file mode 100644 index 0000000..919f8d8 --- /dev/null +++ b/backend/src/routes/index.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import authRoutes from './auth.routes'; +import usersRoutes from './users.routes'; +import customersRoutes from './customers.routes'; +import projectsRoutes from './projects.routes'; +import tasksRoutes from './tasks.routes'; +import equipmentRoutes from './equipment.routes'; +import rmaRoutes from './rma.routes'; +import settingsRoutes from './settings.routes'; +import dashboardRoutes from './dashboard.routes'; + +const router = Router(); + +router.use('/auth', authRoutes); +router.use('/users', usersRoutes); +router.use('/customers', customersRoutes); +router.use('/projects', projectsRoutes); +router.use('/tasks', tasksRoutes); +router.use('/equipment', equipmentRoutes); +router.use('/rma', rmaRoutes); +router.use('/settings', settingsRoutes); +router.use('/dashboard', dashboardRoutes); + +export default router; diff --git a/backend/src/routes/projects.routes.ts b/backend/src/routes/projects.routes.ts new file mode 100644 index 0000000..080536e --- /dev/null +++ b/backend/src/routes/projects.routes.ts @@ -0,0 +1,30 @@ +import { Router } from 'express'; +import * as projectsController from '../controllers/projects.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { projectSchema } from '../utils/validators'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +router.get('/', canRead('projects'), projectsController.getProjects); +router.post('/', canCreate('projects'), validate(projectSchema), projectsController.createProject); +router.get('/:id', canRead('projects'), projectsController.getProject); +router.put('/:id', canUpdate('projects'), projectsController.updateProject); +router.delete('/:id', canDelete('projects'), projectsController.deleteProject); + +// Status +router.patch('/:id/status', canUpdate('projects'), projectsController.updateProjectStatus); + +// Tasks +router.get('/:id/tasks', canRead('projects'), projectsController.getProjectTasks); + +// Members +router.post('/:id/members', canUpdate('projects'), projectsController.addProjectMember); +router.delete('/:id/members/:userId', canUpdate('projects'), projectsController.removeProjectMember); + +export default router; diff --git a/backend/src/routes/rma.routes.ts b/backend/src/routes/rma.routes.ts new file mode 100644 index 0000000..1786067 --- /dev/null +++ b/backend/src/routes/rma.routes.ts @@ -0,0 +1,30 @@ +import { Router } from 'express'; +import * as rmaController from '../controllers/rma.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { canRead, canCreate, canUpdate, canDelete, isAdmin } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { rmaSchema } from '../utils/validators'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +router.get('/', canRead('rma'), rmaController.getRMAs); +router.post('/', canCreate('rma'), validate(rmaSchema), rmaController.createRMA); +router.get('/generate-number', canCreate('rma'), rmaController.generateRMANumberEndpoint); +router.get('/:id', canRead('rma'), rmaController.getRMA); +router.put('/:id', canUpdate('rma'), rmaController.updateRMA); +router.delete('/:id', canDelete('rma'), rmaController.deleteRMA); + +// Status +router.patch('/:id/status', canUpdate('rma'), rmaController.updateRMAStatus); + +// Approval (Admin only) +router.patch('/:id/approve', isAdmin, rmaController.approveRMA); + +// Comments +router.post('/:id/comments', canRead('rma'), rmaController.addRMAComment); + +export default router; diff --git a/backend/src/routes/settings.routes.ts b/backend/src/routes/settings.routes.ts new file mode 100644 index 0000000..5effba1 --- /dev/null +++ b/backend/src/routes/settings.routes.ts @@ -0,0 +1,78 @@ +import { Router } from 'express'; +import * as settingsController from '../controllers/settings.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { isRoot } from '../middleware/rbac.middleware'; + +const router = Router(); + +// Všetky endpointy vyžadujú autentifikáciu +router.use(authenticate); + +// === VEREJNÉ ENDPOINTY (pre všetkých prihlásených) === +// Task Statuses - čítanie +router.get('/task-statuses', settingsController.getTaskStatuses); +// Priorities - čítanie +router.get('/priorities', settingsController.getPriorities); +// Equipment Types - čítanie +router.get('/equipment-types', settingsController.getEquipmentTypes); +// Revision Types - čítanie +router.get('/revision-types', settingsController.getRevisionTypes); +// RMA Statuses - čítanie +router.get('/rma-statuses', settingsController.getRMAStatuses); +// RMA Solutions - čítanie +router.get('/rma-solutions', settingsController.getRMASolutions); +// Tags - čítanie +router.get('/tags', settingsController.getTags); +// User Roles - čítanie +router.get('/roles', settingsController.getUserRoles); + +// === ROOT-ONLY ENDPOINTY (vytvorenie, úprava, mazanie) === +router.use(isRoot); + +// Equipment Types - správa +router.post('/equipment-types', settingsController.createEquipmentType); +router.put('/equipment-types/:id', settingsController.updateEquipmentType); +router.delete('/equipment-types/:id', settingsController.deleteEquipmentType); + +// Revision Types - správa +router.post('/revision-types', settingsController.createRevisionType); +router.put('/revision-types/:id', settingsController.updateRevisionType); +router.delete('/revision-types/:id', settingsController.deleteRevisionType); + +// RMA Statuses - správa +router.post('/rma-statuses', settingsController.createRMAStatus); +router.put('/rma-statuses/:id', settingsController.updateRMAStatus); +router.delete('/rma-statuses/:id', settingsController.deleteRMAStatus); + +// RMA Solutions - správa +router.post('/rma-solutions', settingsController.createRMASolution); +router.put('/rma-solutions/:id', settingsController.updateRMASolution); +router.delete('/rma-solutions/:id', settingsController.deleteRMASolution); + +// Task Statuses - správa +router.post('/task-statuses', settingsController.createTaskStatus); +router.put('/task-statuses/:id', settingsController.updateTaskStatus); +router.delete('/task-statuses/:id', settingsController.deleteTaskStatus); + +// Priorities - správa +router.post('/priorities', settingsController.createPriority); +router.put('/priorities/:id', settingsController.updatePriority); +router.delete('/priorities/:id', settingsController.deletePriority); + +// Tags - správa +router.post('/tags', settingsController.createTag); +router.put('/tags/:id', settingsController.updateTag); +router.delete('/tags/:id', settingsController.deleteTag); + +// User Roles - správa +router.post('/roles', settingsController.createUserRole); +router.put('/roles/:id', settingsController.updateUserRole); +router.delete('/roles/:id', settingsController.deleteUserRole); + +// System Settings - len ROOT +router.get('/system', settingsController.getSystemSettings); +router.get('/system/category/:category', settingsController.getSystemSettingsByCategory); +router.get('/system/:key', settingsController.getSystemSetting); +router.put('/system/:key', settingsController.updateSystemSetting); + +export default router; diff --git a/backend/src/routes/tasks.routes.ts b/backend/src/routes/tasks.routes.ts new file mode 100644 index 0000000..9210463 --- /dev/null +++ b/backend/src/routes/tasks.routes.ts @@ -0,0 +1,31 @@ +import { Router } from 'express'; +import * as tasksController from '../controllers/tasks.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; +import { validate } from '../middleware/validate.middleware'; +import { taskSchema } from '../utils/validators'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +router.get('/', canRead('tasks'), tasksController.getTasks); +router.post('/', canCreate('tasks'), validate(taskSchema), tasksController.createTask); +router.get('/:id', canRead('tasks'), tasksController.getTask); +router.put('/:id', canUpdate('tasks'), tasksController.updateTask); +router.delete('/:id', canDelete('tasks'), tasksController.deleteTask); + +// Status +router.patch('/:id/status', canUpdate('tasks'), tasksController.updateTaskStatus); + +// Assignees +router.post('/:id/assignees', canUpdate('tasks'), tasksController.addTaskAssignee); +router.delete('/:id/assignees/:userId', canUpdate('tasks'), tasksController.removeTaskAssignee); + +// Comments +router.get('/:id/comments', canRead('tasks'), tasksController.getTaskComments); +router.post('/:id/comments', canRead('tasks'), tasksController.addTaskComment); + +export default router; diff --git a/backend/src/routes/users.routes.ts b/backend/src/routes/users.routes.ts new file mode 100644 index 0000000..2da1838 --- /dev/null +++ b/backend/src/routes/users.routes.ts @@ -0,0 +1,21 @@ +import { Router } from 'express'; +import * as usersController from '../controllers/users.controller'; +import { authenticate } from '../middleware/auth.middleware'; +import { isAdmin } from '../middleware/rbac.middleware'; +import { activityLogger } from '../middleware/activityLog.middleware'; + +const router = Router(); + +router.use(authenticate); +router.use(activityLogger); + +// Jednoduchý zoznam pre select/dropdown - prístupné všetkým autentifikovaným +router.get('/simple', usersController.getUsersSimple); + +router.get('/', isAdmin, usersController.getUsers); +router.get('/:id', isAdmin, usersController.getUser); +router.put('/:id', isAdmin, usersController.updateUser); +router.delete('/:id', isAdmin, usersController.deleteUser); +router.patch('/:id/role', isAdmin, usersController.updateUserRole); + +export default router; diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts new file mode 100644 index 0000000..51057c0 --- /dev/null +++ b/backend/src/services/auth.service.ts @@ -0,0 +1,195 @@ +import bcrypt from 'bcryptjs'; +import prisma from '../config/database'; +import { generateAccessToken, generateRefreshToken, verifyRefreshToken, TokenPayload } from '../config/jwt'; +import { ApiError } from '../middleware/errorHandler'; + +interface RegisterInput { + email: string; + password: string; + name: string; + roleId?: string; +} + +interface LoginInput { + email: string; + password: string; +} + +interface AuthTokens { + accessToken: string; + refreshToken: string; +} + +interface UserWithRole { + id: string; + email: string; + name: string; + active: boolean; + role: { + id: string; + code: string; + name: string; + permissions: unknown; + }; +} + +export class AuthService { + async register(data: RegisterInput): Promise { + // Check if user already exists + const existingUser = await prisma.user.findUnique({ + where: { email: data.email }, + }); + + if (existingUser) { + throw new ApiError('Používateľ s týmto emailom už existuje.', 409); + } + + // Get default role (USER) if not specified + let roleId = data.roleId; + if (!roleId) { + const defaultRole = await prisma.userRole.findFirst({ + where: { code: 'USER', active: true }, + }); + + if (!defaultRole) { + throw new ApiError('Predvolená rola nie je nakonfigurovaná.', 500); + } + + roleId = defaultRole.id; + } + + // Hash password + const hashedPassword = await bcrypt.hash(data.password, 10); + + // Create user + const user = await prisma.user.create({ + data: { + email: data.email, + password: hashedPassword, + name: data.name, + roleId, + }, + include: { + role: { + select: { + id: true, + code: true, + name: true, + permissions: true, + }, + }, + }, + }); + + return user; + } + + async login(data: LoginInput): Promise<{ user: UserWithRole; tokens: AuthTokens }> { + // Find user + const user = await prisma.user.findUnique({ + where: { email: data.email }, + include: { + role: { + select: { + id: true, + code: true, + name: true, + permissions: true, + }, + }, + }, + }); + + if (!user) { + throw new ApiError('Neplatný email alebo heslo.', 401); + } + + if (!user.active) { + throw new ApiError('Váš účet je deaktivovaný.', 403); + } + + // Verify password + const isPasswordValid = await bcrypt.compare(data.password, user.password); + + if (!isPasswordValid) { + throw new ApiError('Neplatný email alebo heslo.', 401); + } + + // Generate tokens + const tokenPayload: TokenPayload = { + userId: user.id, + email: user.email, + roleId: user.role.id, + roleCode: user.role.code, + }; + + const tokens: AuthTokens = { + accessToken: generateAccessToken(tokenPayload), + refreshToken: generateRefreshToken(tokenPayload), + }; + + return { user, tokens }; + } + + async refreshTokens(refreshToken: string): Promise { + try { + const decoded = verifyRefreshToken(refreshToken); + + // Verify user still exists and is active + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + include: { + role: { + select: { + id: true, + code: true, + }, + }, + }, + }); + + if (!user || !user.active) { + throw new ApiError('Používateľ neexistuje alebo je neaktívny.', 403); + } + + // Generate new tokens + const tokenPayload: TokenPayload = { + userId: user.id, + email: user.email, + roleId: user.role.id, + roleCode: user.role.code, + }; + + return { + accessToken: generateAccessToken(tokenPayload), + refreshToken: generateRefreshToken(tokenPayload), + }; + } catch { + throw new ApiError('Neplatný refresh token.', 401); + } + } + + async getMe(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + role: { + select: { + id: true, + code: true, + name: true, + permissions: true, + }, + }, + }, + }); + + if (!user) { + throw new ApiError('Používateľ nebol nájdený.', 404); + } + + return user; + } +} + +export const authService = new AuthService(); diff --git a/backend/src/services/config.service.ts b/backend/src/services/config.service.ts new file mode 100644 index 0000000..6da6a15 --- /dev/null +++ b/backend/src/services/config.service.ts @@ -0,0 +1,211 @@ +import prisma from '../config/database'; + +type CacheValue = { + data: unknown; + expiry: number; +}; + +export class ConfigService { + private cache = new Map(); + private cacheExpiry = 60000; // 1 minute + + private getCached(key: string): T | null { + const cached = this.cache.get(key); + if (cached && cached.expiry > Date.now()) { + return cached.data as T; + } + this.cache.delete(key); + return null; + } + + private setCache(key: string, data: unknown): void { + this.cache.set(key, { + data, + expiry: Date.now() + this.cacheExpiry, + }); + } + + async getEquipmentTypes(activeOnly = true) { + const cacheKey = `equipment_types_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const types = await prisma.equipmentType.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, types); + return types; + } + + async getRevisionTypes(activeOnly = true) { + const cacheKey = `revision_types_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const types = await prisma.revisionType.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, types); + return types; + } + + async getRMAStatuses(activeOnly = true) { + const cacheKey = `rma_statuses_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const statuses = await prisma.rMAStatus.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, statuses); + return statuses; + } + + async getRMASolutions(activeOnly = true) { + const cacheKey = `rma_solutions_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const solutions = await prisma.rMASolution.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, solutions); + return solutions; + } + + async getTaskStatuses(activeOnly = true) { + const cacheKey = `task_statuses_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const statuses = await prisma.taskStatus.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, statuses); + return statuses; + } + + async getPriorities(activeOnly = true) { + const cacheKey = `priorities_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const priorities = await prisma.priority.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, priorities); + return priorities; + } + + async getUserRoles(activeOnly = true) { + const cacheKey = `user_roles_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const roles = await prisma.userRole.findMany({ + where: activeOnly ? { active: true } : {}, + orderBy: { level: 'asc' }, + }); + + this.setCache(cacheKey, roles); + return roles; + } + + async getTags(entityType?: string, activeOnly = true) { + const cacheKey = `tags_${entityType || 'all'}_${activeOnly}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const tags = await prisma.tag.findMany({ + where: { + ...(activeOnly && { active: true }), + ...(entityType && { entityType }), + }, + orderBy: { order: 'asc' }, + }); + + this.setCache(cacheKey, tags); + return tags; + } + + async getSetting(key: string): Promise { + const cacheKey = `setting_${key}`; + + const cached = this.getCached(cacheKey); + if (cached !== null) return cached; + + const setting = await prisma.systemSetting.findUnique({ + where: { key }, + }); + + if (setting) { + this.setCache(cacheKey, setting.value); + return setting.value as T; + } + + return null; + } + + async getSettingsByCategory(category: string) { + const cacheKey = `settings_category_${category}`; + + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const settings = await prisma.systemSetting.findMany({ + where: { category }, + }); + + this.setCache(cacheKey, settings); + return settings; + } + + async getInitialTaskStatus() { + const statuses = await this.getTaskStatuses(); + return (statuses as { id: string; isInitial: boolean }[]).find((s) => s.isInitial); + } + + async getInitialRMAStatus() { + const statuses = await this.getRMAStatuses(); + return (statuses as { id: string; isInitial: boolean }[]).find((s) => s.isInitial); + } + + async getDefaultPriority() { + const priorities = await this.getPriorities(); + return (priorities as { id: string; code: string }[]).find((p) => p.code === 'MEDIUM'); + } + + clearCache(): void { + this.cache.clear(); + } + + clearCacheKey(keyPrefix: string): void { + for (const key of this.cache.keys()) { + if (key.startsWith(keyPrefix)) { + this.cache.delete(key); + } + } + } +} + +export const configService = new ConfigService(); diff --git a/backend/src/utils/helpers.ts b/backend/src/utils/helpers.ts new file mode 100644 index 0000000..0caf078 --- /dev/null +++ b/backend/src/utils/helpers.ts @@ -0,0 +1,73 @@ +import { Request, Response } from 'express'; + +export const getParam = (req: Request, name: string): string => { + const value = req.params[name]; + return Array.isArray(value) ? value[0] : value; +}; + +export const getQueryString = (req: Request, name: string): string | undefined => { + const value = req.query[name]; + if (value === undefined) return undefined; + return Array.isArray(value) ? String(value[0]) : String(value); +}; + +export const successResponse = ( + res: Response, + data: T, + message = 'Success', + statusCode = 200 +) => { + return res.status(statusCode).json({ + success: true, + message, + data, + }); +}; + +export const errorResponse = ( + res: Response, + message: string, + statusCode = 400, + errors?: unknown +) => { + return res.status(statusCode).json({ + success: false, + message, + errors, + }); +}; + +export const paginatedResponse = ( + res: Response, + data: T[], + total: number, + page: number, + limit: number +) => { + return res.status(200).json({ + success: true, + data, + pagination: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + }, + }); +}; + +export const parseQueryInt = (value: unknown, defaultValue: number): number => { + if (typeof value === 'string') { + const parsed = parseInt(value, 10); + return isNaN(parsed) ? defaultValue : parsed; + } + return defaultValue; +}; + +export const parseBooleanQuery = (value: unknown): boolean | undefined => { + if (value === 'true') return true; + if (value === 'false') return false; + return undefined; +}; diff --git a/backend/src/utils/validators.ts b/backend/src/utils/validators.ts new file mode 100644 index 0000000..9fc4bc2 --- /dev/null +++ b/backend/src/utils/validators.ts @@ -0,0 +1,183 @@ +import { z } from 'zod'; + +// Auth validators +export const loginSchema = z.object({ + email: z.string().email('Neplatný email'), + password: z.string().min(1, 'Heslo je povinné'), +}); + +export const registerSchema = z.object({ + email: z.string().email('Neplatný email'), + password: z + .string() + .min(8, 'Heslo musí mať aspoň 8 znakov') + .regex(/[A-Z]/, 'Heslo musí obsahovať veľké písmeno') + .regex(/[0-9]/, 'Heslo musí obsahovať číslo'), + name: z.string().min(2, 'Meno musí mať aspoň 2 znaky'), + roleId: z.string().optional(), +}); + +// User validators +export const updateUserSchema = z.object({ + email: z.string().email('Neplatný email').optional(), + name: z.string().min(2, 'Meno musí mať aspoň 2 znaky').optional(), + active: z.boolean().optional(), +}); + +// Customer validators +export const customerSchema = z.object({ + name: z.string().min(1, 'Názov je povinný'), + address: z.string().optional(), + email: z.string().email('Neplatný email').optional().or(z.literal('')), + phone: z.string().optional(), + ico: z.string().optional(), + dic: z.string().optional(), + icdph: z.string().optional(), + contactPerson: z.string().optional(), + contactEmail: z.string().email('Neplatný email').optional().or(z.literal('')), + contactPhone: z.string().optional(), + notes: z.string().optional(), +}); + +// Project validators +export const projectSchema = z.object({ + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + customerId: z.string().optional(), + statusId: z.string().optional(), + softDeadline: z.string().datetime().optional().or(z.literal('')), + hardDeadline: z.string().datetime().optional().or(z.literal('')), +}); + +// Task validators +export const taskSchema = z.object({ + title: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + projectId: z.string().optional(), + parentId: z.string().optional(), + statusId: z.string().optional(), + priorityId: z.string().optional(), + deadline: z.string().datetime().optional().or(z.literal('')), + assigneeIds: z.array(z.string()).optional(), +}); + +// Equipment validators +export const equipmentSchema = z.object({ + name: z.string().min(1, 'Názov je povinný'), + typeId: z.string().min(1, 'Typ je povinný'), + brand: z.string().optional(), + model: z.string().optional(), + customerId: z.string().optional(), + address: z.string().min(1, 'Adresa je povinná'), + location: z.string().optional(), + partNumber: z.string().optional(), + serialNumber: z.string().optional(), + installDate: z.string().datetime().optional().or(z.literal('')), + warrantyEnd: z.string().datetime().optional().or(z.literal('')), + warrantyStatus: z.string().optional(), + description: z.string().optional(), + notes: z.string().optional(), +}); + +// RMA validators +export const rmaSchema = z.object({ + customerId: z.string().optional(), + customerName: z.string().optional(), + customerAddress: z.string().optional(), + customerEmail: z.string().email().optional().or(z.literal('')), + customerPhone: z.string().optional(), + customerICO: z.string().optional(), + submittedBy: z.string().min(1, 'Meno odosielateľa je povinné'), + productName: z.string().min(1, 'Názov produktu je povinný'), + invoiceNumber: z.string().optional(), + purchaseDate: z.string().datetime().optional().or(z.literal('')), + productNumber: z.string().optional(), + serialNumber: z.string().optional(), + accessories: z.string().optional(), + issueDescription: z.string().min(1, 'Popis problému je povinný'), + statusId: z.string().optional(), + proposedSolutionId: z.string().optional(), +}); + +// Settings validators +export const equipmentTypeSchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + color: z.string().optional(), + icon: z.string().optional(), + order: z.number().optional(), + active: z.boolean().optional(), +}); + +export const revisionTypeSchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + intervalDays: z.number().min(0, 'Interval musí byť kladné číslo'), + reminderDays: z.number().min(0).optional(), + color: z.string().optional(), + description: z.string().optional(), + order: z.number().optional(), + active: z.boolean().optional(), +}); + +export const taskStatusSchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + color: z.string().optional(), + icon: z.string().optional(), + order: z.number().optional(), + swimlaneColumn: z.string().optional(), + isInitial: z.boolean().optional(), + isFinal: z.boolean().optional(), + canTransitionTo: z.array(z.string()).optional(), + active: z.boolean().optional(), +}); + +export const prioritySchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + color: z.string().optional(), + icon: z.string().optional(), + level: z.number().min(1).max(10), + order: z.number().optional(), + active: z.boolean().optional(), +}); + +export const userRoleSchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + permissions: z.record(z.string(), z.array(z.string())), + level: z.number().min(1), + order: z.number().optional(), + active: z.boolean().optional(), +}); + +export const systemSettingSchema = z.object({ + key: z.string().min(1, 'Kľúč je povinný'), + value: z.unknown(), + category: z.string().min(1, 'Kategória je povinná'), + label: z.string().min(1, 'Label je povinný'), + description: z.string().optional(), + dataType: z.enum(['string', 'number', 'boolean', 'json']), + validation: z.record(z.string(), z.unknown()).optional(), +}); + +export const tagSchema = z.object({ + code: z.string().min(1, 'Kód je povinný'), + name: z.string().min(1, 'Názov je povinný'), + description: z.string().optional(), + color: z.string().optional(), + entityType: z.enum(['PROJECT', 'TASK', 'EQUIPMENT', 'RMA']), + order: z.number().optional(), + active: z.boolean().optional(), +}); + +// Pagination +export const paginationSchema = z.object({ + page: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 1)), + limit: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 20)), +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..687bce3 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/backend/uploads/.gitkeep b/backend/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3bb6207 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Helpdesk + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..1968c10 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4363 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@tanstack/react-query": "^5.90.20", + "axios": "^1.13.4", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.13.0", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", + "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.2", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", + "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2cf6d37 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@tanstack/react-query": "^5.90.20", + "axios": "^1.13.4", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.13.0", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..b14a18c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,124 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Toaster } from 'react-hot-toast'; +import { useEffect } from 'react'; + +import { useAuthStore } from '@/store/authStore'; +import { useConfigStore } from '@/store/configStore'; +import { MainLayout } from '@/components/layout'; +import { LoadingOverlay } from '@/components/ui'; + +import { Login } from '@/pages/Login'; +import { Dashboard } from '@/pages/Dashboard'; +import { CustomersList } from '@/pages/customers'; +import { ProjectsList } from '@/pages/projects'; +import { TasksList } from '@/pages/tasks'; +import { EquipmentList } from '@/pages/equipment'; +import { RMAList } from '@/pages/rma'; +import { SettingsDashboard } from '@/pages/settings'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 1, + }, + }, +}); + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, user, isLoading, fetchProfile } = useAuthStore(); + const { fetchConfig, isLoaded: configLoaded } = useConfigStore(); + + useEffect(() => { + // Only fetch profile if we have a token but no user data + const token = localStorage.getItem('accessToken'); + if (token && !user) { + fetchProfile(); + } + }, [user, fetchProfile]); + + useEffect(() => { + if (isAuthenticated && !configLoaded) { + fetchConfig(); + } + }, [isAuthenticated, configLoaded, fetchConfig]); + + // Show loading only when we're actively fetching + if (isLoading && !user) { + return ; + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} + +function RootOnlyRoute({ children }: { children: React.ReactNode }) { + const { user } = useAuthStore(); + + if (user?.role.code !== 'ROOT') { + return ; + } + + return <>{children}; +} + +function AppRoutes() { + const { isAuthenticated } = useAuthStore(); + + return ( + + : } + /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + } /> + + ); +} + +export function App() { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..0b9b46b --- /dev/null +++ b/frontend/src/components/layout/Header.tsx @@ -0,0 +1,46 @@ +import { Link } from 'react-router-dom'; +import { LogOut, User, Settings } from 'lucide-react'; +import { useAuthStore } from '@/store/authStore'; +import { Button } from '@/components/ui'; + +export function Header() { + const { user, logout } = useAuthStore(); + + const handleLogout = async () => { + await logout(); + }; + + return ( +
+
+ + Helpdesk + + +
+ {user && ( + <> +
+ + {user.name} + ({user.role.name}) +
+ + {user.role.code === 'ROOT' && ( + + + + )} + + + + )} +
+
+
+ ); +} diff --git a/frontend/src/components/layout/MainLayout.tsx b/frontend/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..ab68da3 --- /dev/null +++ b/frontend/src/components/layout/MainLayout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from 'react-router-dom'; +import { Header } from './Header'; +import { Sidebar } from './Sidebar'; + +export function MainLayout() { + return ( +
+
+ +
+ +
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..fce6551 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,46 @@ +import { NavLink } from 'react-router-dom'; +import { + LayoutDashboard, + FolderKanban, + CheckSquare, + Users, + Wrench, + RotateCcw, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const navItems = [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, + { to: '/projects', icon: FolderKanban, label: 'Projekty' }, + { to: '/tasks', icon: CheckSquare, label: 'Úlohy' }, + { to: '/customers', icon: Users, label: 'Zákazníci' }, + { to: '/equipment', icon: Wrench, label: 'Zariadenia' }, + { to: '/rma', icon: RotateCcw, label: 'RMA' }, +]; + +export function Sidebar() { + return ( + + ); +} diff --git a/frontend/src/components/layout/index.ts b/frontend/src/components/layout/index.ts new file mode 100644 index 0000000..3b99f87 --- /dev/null +++ b/frontend/src/components/layout/index.ts @@ -0,0 +1,3 @@ +export { Header } from './Header'; +export { Sidebar } from './Sidebar'; +export { MainLayout } from './MainLayout'; diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx new file mode 100644 index 0000000..3c7d240 --- /dev/null +++ b/frontend/src/components/ui/Badge.tsx @@ -0,0 +1,31 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +interface BadgeProps extends HTMLAttributes { + variant?: 'default' | 'secondary' | 'destructive' | 'outline'; + color?: string; + children: ReactNode; +} + +export function Badge({ className, variant = 'default', color, children, style, ...props }: BadgeProps) { + const variants = { + default: 'border-transparent bg-primary text-primary-foreground', + secondary: 'border-transparent bg-secondary text-secondary-foreground', + destructive: 'border-transparent bg-destructive text-destructive-foreground', + outline: 'text-foreground', + }; + + const customStyle = color + ? { backgroundColor: color, borderColor: color, color: '#fff', ...style } + : style; + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..c22ab34 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,41 @@ +import { forwardRef, type ButtonHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; +} + +export const Button = forwardRef( + ({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => { + const variants = { + primary: 'btn-primary', + secondary: 'btn-secondary', + destructive: 'btn-destructive', + outline: 'btn-outline', + ghost: 'btn-ghost', + }; + + const sizes = { + sm: 'btn-sm', + md: 'btn-md', + lg: 'btn-lg', + }; + + return ( + + ); + } +); + +Button.displayName = 'Button'; diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx new file mode 100644 index 0000000..c45e0e2 --- /dev/null +++ b/frontend/src/components/ui/Card.tsx @@ -0,0 +1,74 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +interface CardProps extends HTMLAttributes { + children: ReactNode; +} + +export function Card({ className, children, ...props }: CardProps) { + return ( +
+ {children} +
+ ); +} + +interface CardHeaderProps extends HTMLAttributes { + children: ReactNode; +} + +export function CardHeader({ className, children, ...props }: CardHeaderProps) { + return ( +
+ {children} +
+ ); +} + +interface CardTitleProps extends HTMLAttributes { + children: ReactNode; +} + +export function CardTitle({ className, children, ...props }: CardTitleProps) { + return ( +

+ {children} +

+ ); +} + +interface CardDescriptionProps extends HTMLAttributes { + children: ReactNode; +} + +export function CardDescription({ className, children, ...props }: CardDescriptionProps) { + return ( +

+ {children} +

+ ); +} + +interface CardContentProps extends HTMLAttributes { + children: ReactNode; +} + +export function CardContent({ className, children, ...props }: CardContentProps) { + return ( +
+ {children} +
+ ); +} + +interface CardFooterProps extends HTMLAttributes { + children: ReactNode; +} + +export function CardFooter({ className, children, ...props }: CardFooterProps) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx new file mode 100644 index 0000000..21379ed --- /dev/null +++ b/frontend/src/components/ui/Input.tsx @@ -0,0 +1,34 @@ +import { forwardRef, type InputHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export interface InputProps extends InputHTMLAttributes { + error?: string; + label?: string; +} + +export const Input = forwardRef( + ({ className, error, label, id, ...props }, ref) => { + return ( +
+ {label && ( + + )} + + {error &&

{error}

} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 0000000..c6b5a69 --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -0,0 +1,78 @@ +import { useEffect, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import { cn } from '@/lib/utils'; +import { X } from 'lucide-react'; +import { Button } from './Button'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: ReactNode; + className?: string; + size?: 'sm' | 'md' | 'lg' | 'xl'; +} + +export function Modal({ isOpen, onClose, title, children, className, size = 'md' }: ModalProps) { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + const sizes = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + }; + + return createPortal( +
+
+
+ {title && ( +
+

{title}

+ +
+ )} + {children} +
+
, + document.body + ); +} + +interface ModalFooterProps { + children: ReactNode; + className?: string; +} + +export function ModalFooter({ children, className }: ModalFooterProps) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx new file mode 100644 index 0000000..b2e3229 --- /dev/null +++ b/frontend/src/components/ui/Select.tsx @@ -0,0 +1,52 @@ +import { forwardRef, type SelectHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export interface SelectOption { + value: string; + label: string; +} + +export interface SelectProps extends SelectHTMLAttributes { + error?: string; + label?: string; + options: SelectOption[]; + placeholder?: string; +} + +export const Select = forwardRef( + ({ className, error, label, id, options, placeholder, ...props }, ref) => { + return ( +
+ {label && ( + + )} + + {error &&

{error}

} +
+ ); + } +); + +Select.displayName = 'Select'; diff --git a/frontend/src/components/ui/Spinner.tsx b/frontend/src/components/ui/Spinner.tsx new file mode 100644 index 0000000..7b838ec --- /dev/null +++ b/frontend/src/components/ui/Spinner.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; + +interface SpinnerProps { + className?: string; + size?: 'sm' | 'md' | 'lg'; +} + +export function Spinner({ className, size = 'md' }: SpinnerProps) { + const sizes = { + sm: 'h-4 w-4', + md: 'h-6 w-6', + lg: 'h-8 w-8', + }; + + return ; +} + +interface LoadingOverlayProps { + message?: string; +} + +export function LoadingOverlay({ message = 'Načítavam...' }: LoadingOverlayProps) { + return ( +
+
+ +

{message}

+
+
+ ); +} diff --git a/frontend/src/components/ui/Table.tsx b/frontend/src/components/ui/Table.tsx new file mode 100644 index 0000000..200b5ba --- /dev/null +++ b/frontend/src/components/ui/Table.tsx @@ -0,0 +1,85 @@ +import type { HTMLAttributes, ReactNode, ThHTMLAttributes, TdHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +interface TableProps extends HTMLAttributes { + children: ReactNode; +} + +export function Table({ className, children, ...props }: TableProps) { + return ( +
+ + {children} +
+
+ ); +} + +interface TableHeaderProps extends HTMLAttributes { + children: ReactNode; +} + +export function TableHeader({ className, children, ...props }: TableHeaderProps) { + return ( + + {children} + + ); +} + +interface TableBodyProps extends HTMLAttributes { + children: ReactNode; +} + +export function TableBody({ className, children, ...props }: TableBodyProps) { + return ( + + {children} + + ); +} + +interface TableRowProps extends HTMLAttributes { + children: ReactNode; +} + +export function TableRow({ className, children, ...props }: TableRowProps) { + return ( + + {children} + + ); +} + +interface TableHeadProps extends ThHTMLAttributes { + children?: ReactNode; +} + +export function TableHead({ className, children, ...props }: TableHeadProps) { + return ( + + {children} + + ); +} + +interface TableCellProps extends TdHTMLAttributes { + children?: ReactNode; +} + +export function TableCell({ className, children, ...props }: TableCellProps) { + return ( + + {children} + + ); +} diff --git a/frontend/src/components/ui/Textarea.tsx b/frontend/src/components/ui/Textarea.tsx new file mode 100644 index 0000000..6d96634 --- /dev/null +++ b/frontend/src/components/ui/Textarea.tsx @@ -0,0 +1,34 @@ +import { forwardRef, type TextareaHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export interface TextareaProps extends TextareaHTMLAttributes { + error?: string; + label?: string; +} + +export const Textarea = forwardRef( + ({ className, error, label, id, ...props }, ref) => { + return ( +
+ {label && ( + + )} +