# 🎯 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/ADMIN Settings panel - ✅ User Management (CRUD, reset hesla, zmena roly) - ✅ 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[] // Comments & Notifications comments Comment[] notifications Notification[] // 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[] notifications Notification[] @@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 @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([taskId]) @@index([createdAt]) } // ==================== NOTIFICATIONS ==================== model Notification { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) type String // TASK_ASSIGNED, TASK_STATUS_CHANGED, TASK_COMMENT, etc. title String message String // Prázdne pre TASK_COMMENT - text sa načíta z Comment tabuľky // Odkazy na entity taskId String? task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade) rmaId String? rma RMA? @relation(fields: [rmaId], references: [id], onDelete: Cascade) // Dodatočné dáta (JSON) - napr. commentId, actorName, oldStatus, newStatus data Json? isRead Boolean @default(false) readAt DateTime? snoozedUntil DateTime? // Odloženie notifikácie createdAt DateTime @default(now()) @@index([userId, isRead]) @@index([userId, createdAt]) @@index([taskId]) @@index([rmaId]) } // ==================== 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[] notifications Notification[] @@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 (ROOT/ADMIN) ``` GET /api/users // Stránkovaný zoznam (admin only) GET /api/users/simple // Jednoduchý zoznam pre selecty (server-side search: ?search=meno) POST /api/users // Vytvorenie používateľa (admin only) GET /api/users/:id PUT /api/users/:id // Úprava + reset hesla DELETE /api/users/:id // Soft delete (deaktivácia) PATCH /api/users/:id/role // Zmena roly ``` ### 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/ADMIN)** ``` // 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 ``` ### **🆕 Notifications** ``` GET /api/notifications // Zoznam notifikácií (limit, offset, unreadOnly) GET /api/notifications/unread-count // Počet neprečítaných POST /api/notifications/:id/read // Označiť ako prečítané POST /api/notifications/mark-all-read // Označiť všetky ako prečítané POST /api/notifications/:id/snooze // Odložiť notifikáciu (minutes) DELETE /api/notifications/:id // Vymazať notifikáciu ``` ### 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 │ │ │ ├── notifications/ # NEW (Fáza 2) │ │ └── NotificationCenter.tsx # Zvonček s dropdown v header │ │ │ ├── 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 │ │ ├── UserManagement.tsx # Správa používateľov (ROOT/ADMIN) │ │ ├── UserForm.tsx # Formulár vytvorenie/editácia │ │ ├── PasswordResetModal.tsx # Reset hesla │ │ ├── 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) - *PREBIEHAJÚCA* **Cieľ:** Swimlanes, revízie, RMA workflow, reminders, notifikácie **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) - [x] File upload handling - [x] **Notification system** ✅ - [x] Notification model (Prisma) - [x] notification.service.ts - CRUD, enrichment komentárov - [x] notifyTaskComment - ukladá len commentId (žiadna duplicita) - [x] notifyTaskStatusChange - ukladá oldStatus, newStatus, actorName - [x] notifyTaskAssignment - [x] Snooze funkcionalita s konfigurovateľnými možnosťami - [x] SystemSetting NOTIFICATION_SNOOZE_OPTIONS **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) - [x] File attachments - [ ] Comments - [ ] PDF export - [ ] **Inline Quick Actions** - [ ] **Quick Search (Ctrl+K)** - [ ] Reminder management UI - [ ] Filters & tags - [ ] Real-time updates (WebSocket) - [x] **Notification UI** ✅ - [x] NotificationCenter komponent (zvonček v header) - [x] Dashboard - prehľadné zobrazenie notifikácií - [x] Typ notifikácie + relatívny čas - [x] Názov úlohy + projekt - [x] Detail zmeny/komentára + autor - [x] markAsRead pri akcii (komentár/zmena stavu) - [x] Snooze dropdown s konfigurovateľnými možnosťami - [x] useSnoozeOptions hook (načíta z SystemSettings) **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. Notification Service (Fáza 2)** ```typescript // services/notification.service.ts export enum NotificationType { TASK_ASSIGNED = 'TASK_ASSIGNED', TASK_STATUS_CHANGED = 'TASK_STATUS_CHANGED', TASK_COMMENT = 'TASK_COMMENT', TASK_DEADLINE_APPROACHING = 'TASK_DEADLINE_APPROACHING', TASK_UPDATED = 'TASK_UPDATED', RMA_ASSIGNED = 'RMA_ASSIGNED', RMA_STATUS_CHANGED = 'RMA_STATUS_CHANGED', RMA_COMMENT = 'RMA_COMMENT', } export const notificationService = { // Vytvorenie notifikácie pre viacerých používateľov async createForUsers(userIds: string[], data: { type: NotificationType; title: string; message: string; taskId?: string; rmaId?: string; data?: object; // Dodatočné dáta (commentId, actorName, ...) }) { if (userIds.length === 0) return []; return prisma.notification.createMany({ data: userIds.map((userId) => ({ userId, type: data.type, title: data.title, message: data.message, taskId: data.taskId, rmaId: data.rmaId, data: data.data || undefined, })), }); }, // Notifikácia o novom komentári - NEUKLADÁ text, len commentId async notifyTaskComment(taskId: string, commentId: string, commentByUserId: string, commentByUserName: string) { const task = await prisma.task.findUnique({ where: { id: taskId }, include: { assignees: { select: { userId: true } }, createdBy: { select: { id: true } }, }, }); if (!task) return; const userIds = new Set(); task.assignees.forEach((a) => userIds.add(a.userId)); userIds.add(task.createdById); userIds.delete(commentByUserId); // Nenotifikovať autora if (userIds.size === 0) return; await this.createForUsers(Array.from(userIds), { type: NotificationType.TASK_COMMENT, title: 'Nový komentár', message: '', // Text sa načíta z Comment tabuľky (žiadna duplicita) taskId: task.id, data: { commentId, actorName: commentByUserName }, }); }, // Pri načítaní notifikácií - enrichment TASK_COMMENT async getForUser(userId: string, options?: { limit?: number; offset?: number }) { const rawNotifications = await prisma.notification.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, take: options?.limit || 50, skip: options?.offset || 0, include: { task: { select: { id: true, title: true, project: true } }, }, }); // Pre TASK_COMMENT načítaj text komentára z Comment tabuľky const notifications = await Promise.all( rawNotifications.map(async (notification) => { if (notification.type === 'TASK_COMMENT' && notification.taskId) { const data = notification.data as { commentId?: string } | null; if (data?.commentId) { const comment = await prisma.comment.findUnique({ where: { id: data.commentId }, select: { content: true }, }); if (comment) { const shortComment = comment.content.length > 100 ? comment.content.substring(0, 100) + '...' : comment.content; return { ...notification, message: shortComment }; } } } return notification; }) ); return notifications; }, }; ``` --- ### **6. 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 │ │ │ ├── useSnoozeOptions.ts # NEW (Fáza 2) - konfigurovateľné snooze možnosti │ │ │ └── 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 │ │ │ └── notification.api.ts # NEW (Fáza 2) │ │ │ │ │ ├── store/ │ │ │ ├── authStore.ts │ │ │ ├── configStore.ts # NEW │ │ │ ├── projectsStore.ts │ │ │ ├── tasksStore.ts │ │ │ └── notificationStore.ts # NEW (Fáza 2) │ │ │ │ │ ├── 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* *Posledná aktualizácia: 19.02.2026* *Verzia: 2.2.0* *Autor: Claude (Anthropic) + Používateľ*