- Backend: CRUD revízií, schedule endpoint (agregovaný plán), skip revízia, stats - Shared utility revisionSchedule.ts - centralizovaná logika výpočtu cyklov - Equipment detail s revíznym plánom, históriou a prílohami - Frontend: RevisionsList s tabmi (nadchádzajúce/po termíne/vykonané/preskočené) - Pozičné labeling cyklov (eliminuje drift 4×90≠365) - EquipmentRevisionSchedule model (many-to-many typy revízií) - Aktualizovaná dokumentácia HELPDESK_INIT_V2.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 KiB
🎯 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: 23.02.2026
🆕 Novinky vo V2
Hlavné Rozšírenia:
- 🔧 Equipment Management - Evidencia zariadení s revíznymi kontrolami
- 📝 RMA System - Kompletný reklamačný proces
- 👥 Customer Database - Centralizovaná zákaznícka databáza
- ⚙️ Dynamic Configuration - Všetky typy, stavy a nastavenia konfigurovateľné cez GUI
- 🔄 Workflow Engine - Flexibilné workflow pravidlá
- 🏠 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é)
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
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[]
equipmentSchedules EquipmentRevisionSchedule[]
@@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?
revisionCycleStart DateTime? // Anchor pre výpočet revíznych cyklov (default = installDate)
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[]
revisionSchedules EquipmentRevisionSchedule[] // Priradené typy revízií
attachments EquipmentAttachment[]
tags EquipmentTag[]
@@index([typeId])
@@index([customerId])
@@index([warrantyEnd])
@@index([active])
@@index([createdById])
}
// Priradenie revíznych typov k zariadeniu (many-to-many)
model EquipmentRevisionSchedule {
id String @id @default(cuid())
equipmentId String
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
revisionTypeId String
revisionType RevisionType @relation(fields: [revisionTypeId], references: [id])
createdAt DateTime @default(now())
@@unique([equipmentId, revisionTypeId])
@@index([equipmentId])
@@index([revisionTypeId])
}
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])
status String @default("performed") // "performed" | "skipped"
performedDate DateTime
nextDueDate DateTime? // Auto-calculated z cyklového anchoru
performedById String
performedBy User @relation(fields: [performedById], references: [id])
findings String? @db.Text
result String? // "OK", "MINOR_ISSUES", "CRITICAL"
notes String? @db.Text
skipReason String? // Dôvod preskočenia (ak status = "skipped")
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])
@@index([status])
}
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 <token>
→ Middleware verifies JWT
→ Middleware checks user.role permissions
3. Refresh:
POST /api/auth/refresh
4. Logout:
POST /api/auth/logout
RBAC (Dynamic from Database)
// 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 // Zoznam zariadení (stránkovaný, search, filtre)
POST /api/equipment // Vytvorenie zariadenia + revisionSchedules
GET /api/equipment/reminders // Upcoming revisions (PRED /:id!)
GET /api/equipment/:id // Detail zariadenia
PUT /api/equipment/:id // Úprava zariadenia + revisionSchedules
DELETE /api/equipment/:id // Soft delete (deaktivácia)
GET /api/equipment/:id/schedule // Revízny plán zariadenia (nadchádzajúce dátumy)
GET /api/equipment/:id/revisions // História revízií zariadenia
POST /api/equipment/:id/revisions // Pridanie revízie cez equipment detail
GET /api/equipment/:id/files // Zoznam príloh
POST /api/equipment/:id/files // Upload príloh (max 10)
DELETE /api/equipment/:id/files/:fileId // Zmazanie prílohy
🆕 Revisions
GET /api/revisions // Zoznam revízií (stránkovaný, status filter, search)
GET /api/revisions/stats // Štatistiky: { upcoming, overdue, performed, skipped }
GET /api/revisions/schedule // Agregovaný plán zo VŠETKÝCH zariadení (view=upcoming|overdue)
POST /api/revisions/skip // Preskočenie plánovanej revízie (s dôvodom)
GET /api/revisions/:id // Detail revízie
POST /api/revisions // Vytvorenie revízie
PUT /api/revisions/:id // Úprava revízie
DELETE /api/revisions/:id // Zmazanie revízie
Query parametre pre /schedule: view (upcoming|overdue), typeId, customerId, search, page, limit
Body pre /skip:
{
"equipmentId": "string",
"typeId": "string",
"scheduledDate": "ISO date string",
"skipReason": "string (optional)"
}
🆕 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
🔧 Revízny Systém - Architektúra
Princípy
- Cyklový anchor - Revízne termíny sú ukotvené na
revisionCycleStart(aleboinstallDate). Cyklus sa NEposúva keď sa revízia vykoná po termíne. - Pozičné labeling - Cyklové body sa generujú LEN z najkratšieho intervalu (napr. štvrťročný = 90 dní). Pozícia v ročnom cykle určuje label: 1., 2., 3. Štvrťročná, alebo Ročná pri 4. pozícii. Toto eliminuje drift problém (4×90≠365).
- Pokrývajúce typy - Dlhší interval pokrýva kratší (ročná pokrýva štvrťročnú). Po ročnej revízii sa nextDueDate počíta z najkratšieho intervalu.
- Skip mechanizmus - Plánovaná revízia sa dá preskočiť s dôvodom. Vytvorí sa záznam so
status: "skipped".
Seed dáta - Typy revízií
MONTHLY - Mesačná revízia (30 dní, reminder 7 dní)
QUARTERLY - Štvrťročná revízia (90 dní, reminder 14 dní)
BIANNUAL - Polročná revízia (180 dní, reminder 21 dní)
ANNUAL - Ročná revízia (365 dní, reminder 30 dní)
BIENNIAL - Dvojročná revízia (730 dní, reminder 60 dní)
TRIENNIAL - Trojročná revízia (1095 dní, reminder 90 dní)
QUADRENNIAL - Štvorročná revízia (1460 dní, reminder 90 dní)
QUINQUENNIAL - Päťročná revízia (1825 dní, reminder 90 dní)
EMERGENCY - Mimoriadna revízia (0 dní, bez opakovania)
Seed dáta - Typy zariadení
EPS - Elektrická požiarna signalizácia
HSP - Hlasová signalizácia požiaru
EZS - Elektronický zabezpečovací systém
BLESKOZVOD - Bleskozvod
Shared utility: backend/src/utils/revisionSchedule.ts
Centralizovaná logika zdieľaná medzi equipment.controller.ts a revisions.controller.ts:
| Funkcia | Popis |
|---|---|
calculateNextDueDateFromInstall() |
Výpočet nasledujúceho termínu z anchor dátumu a intervalu |
detectSkippedCycles() |
Detekcia preskočených cyklových bodov |
computeFirstUpcomingCycleDate() |
Prvý nadchádzajúci cyklový bod po dátume |
getCoveringTypeIds() |
Nájde typy s dlhším intervalom (pokrývajúce) |
calculateReminderDate() |
Dátum pripomienky = nextDueDate - reminderDays |
getShortestIntervalForEquipment() |
Najkratší interval zo všetkých plánov zariadenia |
computeNextDueAndReminder() |
Kompletný výpočet nextDueDate + reminderDate |
mergeOverlappingItems() |
Zlúčenie prekrývajúcich sa termínov rôznych typov |
Equipment Schedule (GET /api/equipment/:id/schedule)
Vracia:
schedules- Stav každého priradeného revízneho typu (posledná revízia, nasledujúci termín)upcomingDates- Nadchádzajúce dátumy s pozičným labelom (lookAhead = 365 dní)revisionHistory- Posledných N vykonaných/preskočených revízií
Revisions Schedule (GET /api/revisions/schedule)
Agregovaný plán zo VŠETKÝCH zariadení. Pre každý equipment+revisionType pár:
- Ak existuje posledná revízia →
dueDate = latestRevision.nextDueDate - Ak neexistuje → vypočíta sa prvý cyklový bod po dnešku z anchoru
Filtrovateľné podľa: view (upcoming/overdue), typeId, customerId, search
🎨 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 # Detail s revíznym plánom, históriou, prílohami
│ │ ├── EquipmentForm.tsx # Formulár s revisionSchedules (typ revízií)
│ │ └── RevisionForm.tsx
│ │
│ ├── revisions/ # NEW
│ │ ├── RevisionsList.tsx # Hlavný zoznam s tabmi (upcoming/overdue/performed/skipped)
│ │ └── RevisionForm.tsx # Formulár pre pridanie/editáciu revízie
│ │
│ ├── 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)
│
├── hooks/
│ ├── useRevisionStatus.ts # Hook pre stav revízie (schedule/performed/skipped)
│ └── useSnoozeOptions.ts
│
├── services/
│ ├── equipment.api.ts
│ ├── revisions.api.ts # API pre revízie (CRUD, schedule, skip, stats)
│ ├── settings.api.ts
│ └── ...
│
└── types/
└── index.ts # Obsahuje Revision, RevisionScheduleItem, RevisionStats
🚀 Development Fázy (Aktualizované)
FÁZA 1: MVP + Configuration Foundation ✅ DOKONČENÁ
Cieľ: Základná funkcionalita + dynamická konfigurácia
Backend:
- Setup projektu (Express, Prisma, PostgreSQL)
- Databázová schéma V2 (všetky configuration tables)
- JWT Auth (access + refresh tokens)
- RBAC middleware (dynamic permissions)
- User CRUD +
/users/simpleendpoint (server-side search) - User Roles CRUD (dynamické role)
- Project CRUD (s fallback pre default status)
- Task CRUD (s fallback pre default status/priority)
- Task Comments (s kontrolou oprávnení - len autor/priradený)
- Customer CRUD
- Equipment CRUD (s revíznymi plánmi a prílohami)
- RMA CRUD (basic, bez workflow)
- Settings API (CRUD pre všetky config tables)
- Dashboard API (
/dashboard,/dashboard/today,/dashboard/stats) - Activity logging
- Seed data (default config)
Frontend:
- Setup (React 18, Vite, TypeScript, Tailwind CSS v4)
- Auth pages (login s redirect po prihlásení)
- Layout (header, sidebar, MainLayout)
- Dashboard (štatistické karty + moje úlohy + moje projekty)
- Project list/create/edit (s error handling)
- Task list/create/edit (so stĺpcom "Zadal")
- 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)
- UserSelect - server-side vyhľadávanie používateľov (debounce 300ms)
- Customer list/create/edit
- Equipment list/create (basic form)
- RMA list/create (basic form)
- Settings Dashboard (ROOT only)
- Equipment Types
- Revision Types
- RMA Statuses/Solutions
- Task Statuses
- Priorities
- User Roles
- Basic forms (React Hook Form + Zod) s lepším error handling
- Toast notifications (react-hot-toast)
- 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 (s revíznymi plánmi)
✅ 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:
# 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 ✅
- CRUD endpoints (create, read, update, delete)
- Auto-calculate nextDueDate (cyklový anchor, pozičné labeling)
- Skip revision endpoint (preskočenie s dôvodom)
- Agregovaný schedule endpoint (zo všetkých zariadení)
- Stats endpoint (upcoming, overdue, performed, skipped)
- Shared utility
revisionSchedule.ts - Equipment schedule endpoint (nadchádzajúce dátumy s labelmi)
- Reminder scheduler (cron job na posielanie pripomienok)
- 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
- Notification system ✅
- Notification model (Prisma)
- notification.service.ts - CRUD, enrichment komentárov
- notifyTaskComment - ukladá len commentId (žiadna duplicita)
- notifyTaskStatusChange - ukladá oldStatus, newStatus, actorName
- notifyTaskAssignment
- Snooze funkcionalita s konfigurovateľnými možnosťami
- SystemSetting NOTIFICATION_SNOOZE_OPTIONS
Frontend:
- Swimlanes Board (dnd-kit)
- Project swimlanes
- Drag & Drop
- Collapse/expand
- Progress indicators
- Equipment Management ✅
- EquipmentList - zoznam zariadení s filtrami
- EquipmentForm - formulár s revisionSchedules
- EquipmentDetail - detail s revíznym plánom, históriou, prílohami
- RevisionForm - formulár pre pridanie/editáciu revízie
- Revízny plán s pozičným labelingom
- File attachments
- Revisions Management ✅
- RevisionsList - tabmi (nadchádzajúce, po termíne, vykonané, preskočené)
- Filtrovacie tlačidlá podľa typu revízie
- Preskočenie revízie s dôvodom
- Vykonanie revízie z plánu (predvyplnený formulár)
- useRevisionStatus hook
- 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)
- Notification UI ✅
- NotificationCenter komponent (zvonček v header)
- Dashboard - prehľadné zobrazenie notifikácií
- Typ notifikácie + relatívny čas
- Názov úlohy + projekt
- Detail zmeny/komentára + autor
- markAsRead pri akcii (komentár/zmena stavu)
- Snooze dropdown s konfigurovateľnými možnosťami
- useSnoozeOptions hook (načíta z SystemSettings)
Deliverable:
✅ Všetko z Fázy 1 +
⏳ Swimlanes board
✅ Revízny systém (CRUD, plán, skip, schedule, stats)
✅ Equipment management (detail, plán, história, prílohy)
⏳ 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)
# 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)
// 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)
// services/config.service.ts
class ConfigService {
private cache = new Map<string, any>();
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<T = any>(key: string): Promise<T | null> {
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
// services/rma.service.ts
async function generateRMANumber(): Promise<string> {
const format = await configService.getSetting<string>('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
// 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<boolean>(
'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
// 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)
// 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<string>();
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)
// 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)
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:
- Vytvorte repozitár
- Inicializujte backend (Fáza 1)
- Inicializujte frontend (Fáza 1)
- Deploy s Docker Compose
- 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ľ