- 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>
2859 lines
80 KiB
Markdown
2859 lines
80 KiB
Markdown
# 🎯 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ľ*
|