Files
helpdesk-texnet/HELPDESK_INIT_V2.md
pettrop da265ff097 Revízny systém - kompletná implementácia
- 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>
2026-02-23 21:59:23 +01:00

2859 lines
80 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🎯 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:**
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[]
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)
```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 // 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`:**
```json
{
"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
1. **Cyklový anchor** - Revízne termíny sú ukotvené na `revisionCycleStart` (alebo `installDate`). Cyklus sa NEposúva keď sa revízia vykoná po termíne.
2. **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).
3. **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.
4. **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:**
- [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** (s revíznymi plánmi a prílohami)
- [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 (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:**
```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:**
- [x] **Revision system**
- [x] CRUD endpoints (create, read, update, delete)
- [x] Auto-calculate nextDueDate (cyklový anchor, pozičné labeling)
- [x] Skip revision endpoint (preskočenie s dôvodom)
- [x] Agregovaný schedule endpoint (zo všetkých zariadení)
- [x] Stats endpoint (upcoming, overdue, performed, skipped)
- [x] Shared utility `revisionSchedule.ts`
- [x] 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)
- [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
- [x] **Equipment Management**
- [x] EquipmentList - zoznam zariadení s filtrami
- [x] EquipmentForm - formulár s revisionSchedules
- [x] EquipmentDetail - detail s revíznym plánom, históriou, prílohami
- [x] RevisionForm - formulár pre pridanie/editáciu revízie
- [x] Revízny plán s pozičným labelingom
- [x] File attachments
- [x] **Revisions Management**
- [x] RevisionsList - tabmi (nadchádzajúce, po termíne, vykonané, preskočené)
- [x] Filtrovacie tlačidlá podľa typu revízie
- [x] Preskočenie revízie s dôvodom
- [x] Vykonanie revízie z plánu (predvyplnený formulár)
- [x] useRevisionStatus hook
- [ ] **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 (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)
```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<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**
```typescript
// 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**
```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<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**
```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<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)**
```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ľ*