Files
crm-server/DOKUMENTACIA.md
2025-11-25 07:52:31 +01:00

2220 lines
57 KiB
Markdown
Raw 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.
# 📚 CRM SERVER - KOMPLETNÁ DOKUMENTÁCIA
> Vytvorené: 2025-11-21
> Verzia: 1.1
> Backend: Node.js + Express + Drizzle ORM + PostgreSQL
> Frontend: React
---
## 📑 OBSAH
1. [Architektúra Systému](#architektúra-systému)
2. [Services - Biznis Logika](#services)
3. [Controllers - Request Handling](#controllers)
4. [Routes - API Endpointy](#routes)
5. [Utils - Pomocné Funkcie](#utils)
6. [Kompletný Zoznam API](#kompletný-zoznam-api)
7. [Vzťahy medzi Službami](#vzťahy-medzi-službami)
---
## ARCHITEKTÚRA SYSTÉMU
### Stack
- **Backend Framework:** Express.js
- **Database:** PostgreSQL
- **ORM:** Drizzle ORM
- **Auth:** JWT (access + refresh tokens)
- **Validation:** Zod schemas
- **Email:** JMAP protocol (Truemail.sk)
- **Encryption:** AES-256-GCM (email passwords)
- **File Upload:** Multer
- **Time Tracking:** Real-time timer s automatickým stop/start
### Databázové Tabuľky
```
users → userEmailAccounts ← emailAccounts
↓ ↓
| contacts → emails
|
└─→ projectUsers ←─┐
↓ |
companies → projects → todos → notes
timesheets
users → timeEntries → projects/todos/companies
```
**Vzťahy:**
- `projectUsers`: Many-to-many junction table (users ↔ projects)
- `companies``projects`: One-to-many (company môže mať viac projektov)
- `projects``todos`, `notes`, `timesheets`: One-to-many
- `users``todos.assignedTo`: One-to-many (user má assigned úlohy)
- `users``timeEntries`: One-to-many (user má záznamy času)
- `timeEntries``projects`, `todos`, `companies`: Optional Many-to-one
---
## SERVICES
### 1. auth.service.js
**Účel:** Autentifikácia a onboarding používateľov
**Databáza:** `users`, `userEmailAccounts`
**Metódy:**
```javascript
loginWithTempPassword(username, password, ipAddress, userAgent)
Overí credentials
Vráti JWT tokens
Kontroluje či treba zmeniť heslo / linknúť email
setNewPassword(userId, newPassword)
Hashuje heslo (bcrypt)
Nastaví changedPassword = true
Volá: password.js (hashPassword)
linkEmail(userId, email, emailPassword)
Volá: email.service.js (validateJmapCredentials)
Volá: emailAccountService.js (createEmailAccount)
Pripojí email k userovi
skipEmailSetup(userId)
Umožní preskočiť email setup
```
**Volá:**
- `emailAccountService.createEmailAccount()`
- `email.service.validateJmapCredentials()`
- `utils/password.hashPassword()`
- `utils/jwt.generateTokenPair()`
---
### 2. company.service.js
**Účel:** CRUD operácie pre firmy
**Databáza:** `companies`, `projects`, `todos`, `notes`
**Metódy:**
```javascript
getAllCompanies(searchTerm)
SELECT * FROM companies
Filter: ILIKE na name, email, city
ORDER BY createdAt DESC
getCompanyById(companyId)
SELECT * FROM companies WHERE id = ?
Throw NotFoundError ak neexistuje
createCompany(userId, data)
Kontrola duplicity (name)
INSERT INTO companies
Nastaví createdBy = userId
updateCompany(companyId, data)
Volá getCompanyById (check existencie)
UPDATE companies SET ...
deleteCompany(companyId)
DELETE FROM companies WHERE id = ?
CASCADE delete: projects, todos, notes
getCompanyWithRelations(companyId)
Volá getCompanyById()
LEFT JOIN projects WHERE companyId
LEFT JOIN todos WHERE companyId
LEFT JOIN notes WHERE companyId
Vráti všetko v jednom objekte
```
**Volá:**
- Priamo databázu cez Drizzle ORM
- `utils/errors.NotFoundError`
- `utils/errors.ConflictError`
---
### 3. contact.service.js
**Účel:** Správa email kontaktov (per email account)
**Databáza:** `contacts`, `emails`, `companies`
**Metódy:**
```javascript
getContactsForEmailAccount(emailAccountId)
SELECT * FROM contacts WHERE emailAccountId
addContact(emailAccountId, jmapConfig, email, name, notes, addedByUserId)
INSERT INTO contacts
Volá: jmap.service.syncEmailsFromSender()
Reassign: UPDATE emails SET contactId WHERE from = email
getContactById(contactId, emailAccountId)
Verifikuje prístup k accountu
linkCompanyToContact(contactId, emailAccountId, companyId)
UPDATE contacts SET companyId = ?
createCompanyFromContact(contactId, emailAccountId, userId, companyData)
Volá: company.service.createCompany()
UPDATE contacts SET companyId = newCompany.id
```
**Volá:**
- `jmap.service.syncEmailsFromSender()`
- `company.service.createCompany()`
- Databázu (contacts, emails, companies)
---
### 4. project.service.js
**Účel:** Projekty a správa tímov
**Databáza:** `projects`, `companies`, `todos`, `notes`, `timesheets`, `projectUsers`, `users`
**Metódy:**
```javascript
getAllProjects(searchTerm, companyId)
SELECT * FROM projects
Filter: search (name, desc) + companyId
createProject(userId, data)
Validate: company exists (ak je companyId)
INSERT INTO projects
getProjectWithRelations(projectId)
SELECT project
LEFT JOIN company
LEFT JOIN todos
LEFT JOIN notes
LEFT JOIN timesheets
LEFT JOIN projectUsers + users (tím)
Vráti všetko naraz
// Team Management
getProjectUsers(projectId)
SELECT * FROM projectUsers
LEFT JOIN users
WHERE projectId
Vráti: Array<{ id, userId, role, addedBy, addedAt, user: {...} }>
DRIZZLE PATTERN: Používa .select() bez params, pristupuje cez row.table_name.field
assignUserToProject(projectId, userId, addedByUserId, role)
Validate: project exists (getProjectById)
Validate: user exists
Check: nie je assigned (ConflictError)
INSERT INTO projectUsers
Vráti assignment s user details
UNIQUE constraint: projectId + userId
removeUserFromProject(projectId, userId)
Validate: project exists
Check: user je assigned (NotFoundError)
DELETE FROM projectUsers WHERE projectId AND userId
updateUserRoleOnProject(projectId, userId, role)
Validate: project exists
Check: user je assigned
UPDATE projectUsers SET role
Vráti updated assignment s user details
```
**Volá:**
- Databázu (projects, projectUsers, users)
- `utils/errors.NotFoundError`
- `utils/errors.ConflictError`
**Dôležité - Drizzle ORM Query Pattern:**
```javascript
// ✅ SPRÁVNE - Štandardný .select() pre joins
const rawResults = await db
.select()
.from(projectUsers)
.leftJoin(users, eq(projectUsers.userId, users.id))
.where(eq(projectUsers.projectId, projectId))
.orderBy(desc(projectUsers.addedAt));
// Potom mapovať výsledky:
const assignedUsers = rawResults.map((row) => ({
id: row.project_users.id,
userId: row.project_users.userId,
user: row.users ? {
id: row.users.id,
username: row.users.username,
} : null,
}));
// ❌ NESPRÁVNE - Nested object syntax NEFUNGUJE v Drizzle!
const wrong = await db
.select({
user: {
id: users.id,
username: users.username,
}
})
.from(projectUsers)
// → TypeError: Cannot convert undefined or null to object
```
---
### 5. todo.service.js
**Účel:** Správa úloh (tasks)
**Databáza:** `todos`, `projects`, `companies`, `users`, `notes`
**Status Enum:** `['pending', 'in_progress', 'completed', 'cancelled']`
**Metódy:**
```javascript
getAllTodos(filters)
Filtre: search, projectId, companyId, assignedTo, status
SELECT * FROM todos WHERE ...
createTodo(userId, data)
Validate: project/company/assignedTo exists
INSERT INTO todos
Default status: 'pending'
updateTodo(todoId, data)
Ak status = 'completed' nastaví completedAt = NOW()
Ak status != 'completed' zmaže completedAt
UPDATE todos
toggleTodoComplete(todoId)
Toggle: 'completed' 'pending'
Používa sa vo frontende pre checkbox toggle
getTodoWithRelations(todoId)
LEFT JOIN project
LEFT JOIN company
LEFT JOIN assignedUser (users)
LEFT JOIN notes
```
**Volá:**
- Databázu (todos, projects, companies, users)
- `utils/errors.NotFoundError`
**⚠️ DÔLEŽITÉ - Status Field:**
- Databázové pole: `status` (enum)
- **NIE** `completed` (boolean)!
- Frontend check: `todo.status === 'completed'`
- Frontend check: `todo.completed` ❌ (toto pole neexistuje!)
---
### 6. note.service.js
**Účel:** Poznámky s remindermi
**Databáza:** `notes`, `companies`, `projects`, `todos`, `contacts`
**Metódy:**
```javascript
// Frontend Mapping Helper
mapNoteForFrontend(note)
Konvertuje: reminderDate reminderAt
Frontend používa "reminderAt", DB "reminderDate"
getAllNotes(filters)
Filter: search, companyId, projectId, todoId, contactId
Volá mapNoteForFrontend() na výsledky
createNote(userId, data)
Validate: company/project/todo/contact exists
INSERT INTO notes
reminderSent = false
Volá mapNoteForFrontend()
updateNote(noteId, data)
Ak sa zmení reminderDate reset reminderSent = false
Volá mapNoteForFrontend()
// Reminder Management
getPendingReminders()
SELECT * WHERE reminderDate <= NOW() AND reminderSent = false
markReminderAsSent(noteId)
UPDATE notes SET reminderSent = true
getUpcomingRemindersForUser(userId)
WHERE createdBy = userId AND pending reminders
```
**Volá:**
- Databázu (notes, companies, projects, todos, contacts)
- `mapNoteForFrontend()` - internal helper
---
### 7. email-account.service.js
**Účel:** Správa email účtov (many-to-many sharing)
**Databáza:** `emailAccounts`, `userEmailAccounts`, `users`
**Metódy:**
```javascript
getUserEmailAccounts(userId)
SELECT emailAccounts
JOIN userEmailAccounts
WHERE userId
createEmailAccount(userId, email, emailPassword)
Volá: email.service.validateJmapCredentials()
Encrypt password: password.encryptPassword()
Kontrola: email existuje?
- Áno link existujúci account (shared = true)
- Nie vytvor nový account
INSERT INTO userEmailAccounts
Ak prvý account set isPrimary = true
Volá: contact.service.syncContactsForAccount()
getEmailAccountWithCredentials(accountId, userId)
SELECT emailAccount
Decrypt password: password.decryptPassword()
Vráti s plaintext password (pre JMAP)
setPrimaryEmailAccount(accountId, userId)
UPDATE userEmailAccounts SET isPrimary = false (všetky)
UPDATE userEmailAccounts SET isPrimary = true (tento)
removeUserFromEmailAccount(accountId, userId)
DELETE FROM userEmailAccounts
Ak posledný user CASCADE delete emailAccount + data
shareEmailAccountWithUser(accountId, ownerId, targetUserId)
Kontrola: owner prístup
INSERT INTO userEmailAccounts (link target user)
```
**Volá:**
- `email.service.validateJmapCredentials()`
- `password.encryptPassword()`
- `password.decryptPassword()`
- `contact.service.syncContactsForAccount()`
- Databázu (emailAccounts, userEmailAccounts)
---
### 8. crm-email.service.js
**Účel:** Správa emailov (read status, search)
**Databáza:** `emails`, `contacts`
**Metódy:**
```javascript
getEmailsForAccount(emailAccountId)
SELECT emails
LEFT JOIN contacts
WHERE emailAccountId
Iba emaily od pridaných kontaktov
getEmailThread(emailAccountId, threadId)
SELECT emails WHERE threadId
ORDER BY date ASC (konverzácia)
searchEmails(emailAccountId, query)
DB search: ILIKE na subject, body, from
LIMIT 50
getUnreadCountForAccount(emailAccountId)
SELECT COUNT WHERE isRead = false
Iba od added contacts
markThreadAsRead(emailAccountId, threadId)
UPDATE emails SET isRead = true WHERE threadId
markContactEmailsAsRead(contactId, emailAccountId)
Volá: getContactByEmail()
UPDATE emails SET isRead = true WHERE contactId
```
**Volá:**
- Databázu (emails, contacts)
- `getContactByEmail()` - internal
---
### 9. jmap.service.js
**Účel:** JMAP protokol integrácia
**JMAP Server:** `https://mail.truemail.sk/jmap/`
**Metódy:**
```javascript
jmapRequest(jmapConfig, methodCalls)
HTTP POST na JMAP server
Authorization: Basic + password
Vráti raw JMAP response
getMailboxes(jmapConfig)
JMAP call: "Mailbox/get"
Vráti Inbox, Sent, Trash, atď.
syncEmailsFromSender(jmapConfig, emailAccountId, contactId, email)
JMAP query: "Email/query" WHERE from = email
Fetch email details: "Email/get"
INSERT INTO emails (bulk)
Volá: markEmailAsRead() na serveri
searchEmailsJMAP(jmapConfig, emailAccountId, query)
JMAP full-text search
Neukláda do DB, iba vráti results
markEmailAsRead(jmapConfig, jmapId, isRead)
JMAP: "Email/set" update \Seen flag
Sync: UPDATE local DB
sendEmail(jmapConfig, to, subject, body, inReplyTo, threadId)
JMAP: "EmailSubmission/set"
Pošle email cez JMAP server
INSERT do local DB (sentByUserId)
discoverContactsFromJMAP(jmapConfig, emailAccountId, search, limit)
JMAP: "Email/query" group by sender
Vráti unique senderi (potenciálne kontakty)
```
**Volá:**
- JMAP server API (HTTP POST)
- Databázu (emails) pre sync
---
### 10. email.service.js
**Účel:** Validácia JMAP credentials
**Metódy:**
```javascript
validateJmapCredentials(email, password)
HTTP GET na JMAP server /.well-known/jmap
Authorization: Basic email:password
Ak 200 credentials OK
Ak 401 invalid credentials
Vráti accountId a session data
```
**Volá:**
- JMAP server (validácia)
---
### 11. time-tracking.service.js
**Účel:** Sledovanie odpracovaného času
**Databáza:** `timeEntries`, `projects`, `todos`, `companies`, `users`
**Metódy:**
```javascript
startTimeEntry(userId, data)
Validate: project/todo/company exists (ak poskytnuté)
Check: existujúci bežiaci časovač
Ak beží automaticky ho zastaví
INSERT INTO timeEntries
isRunning = true, startTime = NOW()
stopTimeEntry(entryId, userId, data)
Validate: ownership, isRunning = true
Počíta duration v minútach
UPDATE timeEntries SET endTime, duration, isRunning = false
Optional: update projectId, todoId, companyId, description
getRunningTimeEntry(userId)
SELECT * WHERE userId AND isRunning = true
Vráti aktuálny bežiaci časovač (alebo null)
getAllTimeEntries(userId, filters)
Filter: projectId, todoId, companyId, startDate, endDate
SELECT * FROM timeEntries WHERE userId
ORDER BY startTime DESC
getMonthlyTimeEntries(userId, year, month)
SELECT * WHERE userId AND startTime BETWEEN month range
Používa sa pre mesačný prehľad
getTimeEntryById(entryId)
SELECT * WHERE id
Throw NotFoundError ak neexistuje
getTimeEntryWithRelations(entryId)
LEFT JOIN project, todo, company
Vráti všetko v jednom objekte
updateTimeEntry(entryId, userId, data)
Validate: ownership, NOT isRunning (nemožno upraviť bežiaci)
Update: startTime, endTime, projectId, todoId, companyId, description
Prepočíta duration ak sa zmení čas
Set isEdited = true
deleteTimeEntry(entryId, userId)
Validate: ownership, NOT isRunning
DELETE FROM timeEntries
getMonthlyStats(userId, year, month)
Volá getMonthlyTimeEntries()
Počíta štatistiky:
- totalMinutes / totalHours
- daysWorked (unique days)
- averagePerDay
- byProject (čas per projekt)
- byCompany (čas per firma)
generateMonthlyTimesheet(userId, year, month)
Generate Excel (XLSX) report z time entries
Fetch user info: username, firstName, lastName
Fetch completed entries for month (LEFT JOIN projects, todos, companies)
Filter: iba entries s endTime a duration
Počíta: totalMinutes, dailyTotals (per day)
Vytvára Excel workbook cez ExcelJS:
- Header: Timesheet, Name, Period, Generated date
- Table: Date, Project, Todo, Company, Description, Start, End, Duration
- Summary: Daily totals + Overall total
Save to: uploads/timesheets/{userId}/{year}/{month}/timesheet-{period}-{timestamp}.xlsx
INSERT INTO timesheets (isGenerated = true)
Vráti: { timesheet, filePath, entriesCount, totalMinutes, totalHours }
```
**Volá:**
- Databázu (timeEntries, projects, todos, companies, users, timesheets)
- ExcelJS library (workbook generation)
- File system (fs/promises) - save XLSX file
- `utils/errors.NotFoundError`
- `utils/errors.BadRequestError`
**Dôležité poznámky:**
- **Auto-stop:** Pri štarte nového časovača sa automaticky zastaví predchádzajúci
- **Duration:** Ukladá sa v minútach (Math.round)
- **isEdited flag:** Označuje manuálne upravené záznamy
- **isRunning:** Iba jeden časovač môže byť aktívny pre usera
---
### 12. audit.service.js
**Účel:** Audit logging pre compliance
**Databáza:** `auditLogs`
**Metódy:**
```javascript
logAuditEvent(userId, action, resourceType, resourceId, details, ip, userAgent)
INSERT INTO auditLogs
Ukladá všetky parametre
// Špecifické loggery
logLoginAttempt(username, success, ip, userAgent, error)
action = "login_attempt"
logPasswordChange(userId, ip, userAgent)
action = "password_change"
logEmailLink(userId, email, ip, userAgent)
action = "email_linked"
logRoleChange(adminId, userId, oldRole, newRole, ip, userAgent)
action = "role_change"
logUserCreation(adminId, newUserId, username, role, ip, userAgent)
action = "user_created"
```
**Volá:**
- Iba databázu (write-only)
---
### 13. timesheet.controller.js
**Účel:** HTTP vrstva pre timesheet upload/list/download/delete (PDF/Excel)
**Deleguje na:** `services/timesheet.service.js`
**Toky handlerov:**
```javascript
uploadTimesheet(req, res)
timesheetService.uploadTimesheet({ userId, year, month, file })
Vráti sanitized meta (bez filePath)
getMyTimesheets(req, res)
timesheetService.getTimesheetsForUser(userId, { year?, month? })
getAllTimesheets(req, res)
timesheetService.getAllTimesheets({ userId?, year?, month? })
downloadTimesheet(req, res)
timesheetService.getDownloadInfo(timesheetId, { userId, role })
res.download(filePath, fileName)
deleteTimesheet(req, res)
timesheetService.deleteTimesheet(timesheetId, { userId, role })
```
**Poznámky:**
- Service vrstva rieši validáciu MIME typu (PDF/XLSX), tvorbu adresárovej štruktúry `uploads/timesheets/{userId}/{year}/{month}`, permission check (owner/admin) a bezpečné mazanie súboru.
- Response payloady obsahujú len meta údaje: `id, fileName, fileType, fileSize, year, month, isGenerated, uploadedAt`.
---
## VALIDATORS
### 1. auth.validators.js
**Účel:** Zod schemas pre autentifikáciu a user management
**Schemas:**
```javascript
loginSchema
username: 3-50 chars, required
password: min 1 char, required
setPasswordSchema
newPassword: min 8 chars, obsahuje a-z, A-Z, 0-9, špeciálny znak
confirmPassword: musí sa zhodovať
.refine() custom validation pre password match
linkEmailSchema (momentálne neexponované; route je vypnutá)
email: valid email format, max 255 chars
emailPassword: min 1 char
createUserSchema (admin only)
username: 3-50 chars, iba [a-zA-Z0-9_-]
email: optional (ak sa zadá, môže sa linknúť JMAP)
emailPassword: optional (pre automatické linkovanie)
firstName, lastName: optional, max 100 chars
updateUserSchema
firstName, lastName, email: all optional
changeRoleSchema
userId: UUID
role: enum ['admin', 'member']
```
**Použitie:**
- Aktívne: `/api/auth/login`, `/api/auth/set-password`, `/api/auth/logout`, `/api/auth/session`
- Neaktivované: `/api/auth/link-email`, `/api/auth/skip-email` (ponechané schema pre prípadné obnovenie)
- Admin user management routes
---
### 2. crm.validators.js
**Účel:** Zod schemas pre Company, Project, Todo, Note, Time Tracking
**Company Schemas:**
```javascript
createCompanySchema
name: required, max 255 chars
description: optional, max 1000 chars
address, city, country: optional
phone: optional, max 50 chars
email: optional, valid email OR empty string
website: optional, valid URL OR empty string
updateCompanySchema
Všetky fields optional
```
**Project Schemas:**
```javascript
createProjectSchema
name: required, max 255 chars
companyId: optional UUID OR empty string
status: enum ['active', 'completed', 'on_hold', 'cancelled']
startDate, endDate: optional strings OR empty
updateProjectSchema
Všetky fields optional
NULL support: .or(z.null())
```
**Todo Schemas:**
```javascript
createTodoSchema
title: required, max 255 chars
projectId, companyId, assignedTo: optional UUID OR empty
status: enum ['pending', 'in_progress', 'completed', 'cancelled']
priority: enum ['low', 'medium', 'high', 'urgent']
dueDate: optional string OR empty
updateTodoSchema
Všetky fields optional + NULL support
```
**Note Schemas:**
```javascript
createNoteSchema
content: required, max 5000 chars
title: optional, max 255 chars
companyId, projectId, todoId, contactId: optional UUID OR empty
reminderDate: optional string OR empty
updateNoteSchema
Všetky fields optional + NULL support
```
**Time Tracking Schemas:**
```javascript
startTimeEntrySchema
projectId, todoId, companyId: optional UUID (preprocessed to null if empty)
description: optional, max 1000 chars, trimmed, null if empty
stopTimeEntrySchema
Same as start (používa sa pre update pri stop)
updateTimeEntrySchema
startTime, endTime: optional ISO strings
projectId, todoId, companyId, description: optional with preprocessing
```
**Helper Functions:**
```javascript
optionalUuid(message)
Preprocess: undefined, null, '' null
Validate: UUID format
Used for optional foreign keys
optionalDescription
Preprocess: trim whitespace, '' null
Validate: max 1000 chars
Nullable
```
**Pattern - Empty String Handling:**
```javascript
// Frontend môže poslať empty string namiesto null
.or(z.literal('')) // Accept empty string
.or(z.literal('').or(z.null())) // Update: accept empty OR null
```
---
### 3. email-account.validators.js
**Účel:** Zod schemas pre email account management
**Schemas:**
```javascript
createEmailAccountSchema
email: required, valid format, max 255 chars
emailPassword: required, min 1 char
updateEmailAccountSchema
emailPassword: optional, min 1 char
isActive: optional boolean
setPrimaryAccountSchema
accountId: UUID
POZNÁMKA: NEVYUŽÍVA SA (endpoint accountId v path params)
```
**Použitie:**
- `/api/email-accounts/*` routes
- JMAP credential validation flow
---
## CONTROLLERS
**Účel:** Spracovanie HTTP requestov, volanie services, vracanie responses
### Zoznam Controllerov:
1. **admin.controller.js** - User management (admin only)
2. **auth.controller.js** - Autentifikácia a onboarding
3. **company.controller.js** - Firmy CRUD + nested notes
4. **contact.controller.js** - Email kontakty
5. **crm-email.controller.js** - Email management (read status, search)
6. **email-account.controller.js** - JMAP účty
7. **note.controller.js** - Standalone poznámky (nevyužité)
8. **project.controller.js** - Projekty CRUD + nested notes + team management
9. **todo.controller.js** - Úlohy CRUD
10. **time-tracking.controller.js** - Sledovanie času
11. **timesheet.controller.js** - Upload a download timesheetov (bez service!)
### Štruktúra každého controllera:
```javascript
export const methodName = async (req, res) => {
try {
// 1. Extract params/body
const { param } = req.params
const data = req.body
const userId = req.userId // z authenticate middleware
// 2. Volaj service
const result = await service.method(param, data)
// 3. Vráť response
res.status(200).json({
success: true,
data: result,
message: 'Success message'
})
} catch (error) {
// 4. Error handling
const errorResponse = formatErrorResponse(error, isDev)
res.status(error.statusCode || 500).json(errorResponse)
}
}
```
### Response Format (štandard):
```json
{
"success": true/false,
"data": { ... },
"count": 10, // optional (pre lists)
"message": "Success/Error message"
}
```
---
## ROUTES
**Účel:** Definícia endpointov, middleware, validácia
### Zoznam Route Files:
1. **admin.routes.js** - User management (Auth + Admin role)
2. **auth.routes.js** - Login, set password (Mixed public/protected)
3. **company.routes.js** - Firmy + nested notes (Auth only)
4. **contact.routes.js** - Kontakty (Auth only)
5. **crm-email.routes.js** - Emaily (Auth only)
6. **email-account.routes.js** - JMAP účty (Auth only)
7. **project.routes.js** - Projekty + notes + team (Auth only)
8. **todo.routes.js** - Úlohy (Auth only)
9. **time-tracking.routes.js** - Time tracking (Auth only)
10. **timesheet.routes.js** - Timesheets upload/download (Auth, admin for /all)
11. **note.routes.js** - Standalone poznámky (odpojené z app.js, ponechané len ako archív)
### Štruktúra route file:
```javascript
import express from 'express'
import * as controller from '../controllers/xxx.controller.js'
import { authenticate } from '../middlewares/auth/authMiddleware.js'
import { validateBody, validateParams } from '../middlewares/security/validateInput.js'
import { schema } from '../validators/xxx.validators.js'
const router = express.Router()
// Global middleware (pre všetky routes)
router.use(authenticate)
// Route definition
router.get('/',
controller.getAll
)
router.post('/',
validateBody(schema), // Zod validation
controller.create
)
router.patch('/:id',
validateParams(z.object({ id: z.string().uuid() })),
validateBody(updateSchema),
controller.update
)
export default router
```
### Middleware poradie:
1. `authenticate` - JWT validácia
2. `validateParams` - Path params validácia (Zod)
3. `validateBody` - Request body validácia (Zod)
4. `controller.method` - Controller handler
---
## UTILS
### Zoznam Utility Files:
1. **errors.js** - Custom error classes + formatting
2. **jwt.js** - JWT token generation and validation
3. **logger.js** - Colored console logging
4. **password.js** - Password hashing, encryption, generation
### 1. errors.js
**Účel:** Custom error classy a formatting
**Error Classes:**
```javascript
class AppError extends Error {
constructor(message, statusCode) {
this.statusCode = statusCode
this.isOperational = true
}
}
class ValidationError extends AppError { statusCode = 400 }
class BadRequestError extends AppError { statusCode = 400 }
class AuthenticationError extends AppError { statusCode = 401 }
class ForbiddenError extends AppError { statusCode = 403 }
class NotFoundError extends AppError { statusCode = 404 }
class ConflictError extends AppError { statusCode = 409 }
class RateLimitError extends AppError { statusCode = 429 }
```
**Funkcie:**
```javascript
formatErrorResponse(error, includeStack)
Formátuje error pre API response
V dev mode: vracia stack trace
V prod mode: iba message
```
**Použitie:**
```javascript
throw new NotFoundError('Company not found')
throw new ConflictError('Email already exists')
```
---
### 2. jwt.js
**Účel:** JWT token management
**Funkcie:**
```javascript
generateAccessToken(payload)
jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' })
generateRefreshToken(payload)
jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: '7d' })
verifyAccessToken(token)
jwt.verify(token, JWT_SECRET)
Vráti decoded payload alebo throw error
generateTokenPair(user)
Vytvorí obe tokens naraz
Payload: { userId, username, role }
```
**Environment Variables:**
```bash
JWT_SECRET=xxx
JWT_REFRESH_SECRET=xxx
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
```
---
### 3. logger.js
**Účel:** Farebné console logging
**Metódy:**
```javascript
logger.info(message, ...args) // 🔵 Blue
logger.success(message, ...args) // 🟢 Green
logger.warn(message, ...args) // 🟡 Yellow
logger.error(message, error) // 🔴 Red
logger.debug(message, ...args) // 🔷 Cyan (dev only)
logger.audit(message, ...args) // 🟣 Magenta
```
**Použitie:**
```javascript
logger.info('Server started', { port: 5000 })
logger.success('User created', { userId })
logger.error('Database error', error)
```
---
### 4. password.js
**Účel:** Password hashing, encryption, generation
**Funkcie:**
```javascript
// Bcrypt (pre user passwords)
hashPassword(password)
bcrypt.hash(password, BCRYPT_ROUNDS)
comparePassword(password, hash)
bcrypt.compare(password, hash)
// AES-256-GCM (pre email passwords)
encryptPassword(text)
crypto.randomBytes(16) // IV
crypto.scrypt(JWT_SECRET) // Key derivation
cipher = crypto.createCipheriv('aes-256-gcm')
Vráti: "iv:authTag:encrypted" (base64)
decryptPassword(encryptedText)
Split "iv:authTag:encrypted"
decipher = crypto.createDecipheriv('aes-256-gcm')
Vráti plaintext
// Temp password generation
generateTempPassword(length = 12)
Random: uppercase + lowercase + numbers + special chars
Použitie: admin vytvára usera
```
**Prečo 2 typy encryption?**
- **Bcrypt (user passwords):** One-way hash, nemožné dekryptovať
- **AES-256 (email passwords):** Reversible, treba plaintext pre JMAP login
---
## KOMPLETNÝ ZOZNAM API
### 🔐 AUTHENTICATION
#### POST /api/auth/login
```
Účel: Prihlásenie používateľa
Body: { username, password }
Response: { user, tokens, needsPasswordChange, needsEmailSetup }
Rate Limit: Áno
```
#### POST /api/auth/set-password
```
Účel: Zmena hesla pri prvom prihlásení
Body: { newPassword }
Auth: Áno
Rate Limit: Áno
```
#### POST /api/auth/logout
```
Účel: Odhlásenie (clear cookies)
Auth: Áno
Response: Clear accessToken & refreshToken cookies
```
#### GET /api/auth/session
```
Účel: Získať info o aktuálnej session
Auth: Áno
Response: { user, authenticated: true }
```
**Removed/disabled:** `/api/auth/link-email`, `/api/auth/skip-email`, `/api/auth/me` (nepoužíva ich FE).
---
### 👥 ADMIN (User Management)
#### POST /api/admin/users
```
Účel: Vytvoriť nového usera
Body: { username, email?, emailPassword?, firstName?, lastName? }
Auth: Áno (admin only)
Response: { user with tempPassword }
Efekt: Vygeneruje temp password, user musí zmeniť pri login
```
#### GET /api/admin/users
```
Účel: Zoznam všetkých userov
Auth: Áno (admin only)
Response: Array of users
```
#### GET /api/admin/users/:userId
```
Účel: Detail usera
Auth: Áno (admin only)
Response: { user with emailAccounts }
```
#### PATCH /api/admin/users/:userId/role
```
Účel: Zmeniť rolu usera
Body: { role: "admin" | "member" }
Auth: Áno (admin only)
```
#### DELETE /api/admin/users/:userId
```
Účel: Zmazať usera
Auth: Áno (admin only)
Efekt: CASCADE delete všetkých dát usera
```
---
### 🏢 COMPANIES
#### GET /api/companies?search=query
```
Účel: Zoznam firiem
Query: search (optional) - hľadá v name, email, city
Auth: Áno
Response: Array of companies
```
#### GET /api/companies/:companyId
```
Účel: Detail firmy
Auth: Áno
Response: Company object
```
> Poznámka: Endpoint `/api/companies/:companyId/details` bol odstránený (frontend používa samostatné volania).
#### POST /api/companies
```
Účel: Vytvoriť firmu
Body: { name*, description, address, city, country, phone, email, website }
Auth: Áno
Validation: createCompanySchema
```
#### PATCH /api/companies/:companyId
```
Účel: Upraviť firmu
Body: (all optional) { name, description, ... }
Auth: Áno
Validation: updateCompanySchema
```
#### DELETE /api/companies/:companyId
```
Účel: Zmazať firmu
Auth: Áno
Efekt: CASCADE delete projects, todos, notes
```
#### GET /api/companies/:companyId/notes
```
Účel: Poznámky firmy
Auth: Áno
Response: Array of notes (mapped reminderDate→reminderAt)
```
#### POST /api/companies/:companyId/notes
```
Účel: Pridať poznámku k firme
Body: { content*, reminderAt? }
Auth: Áno
```
#### PATCH /api/companies/:companyId/notes/:noteId
```
Účel: Upraviť poznámku firmy
Body: { content?, reminderAt? }
Auth: Áno
```
#### DELETE /api/companies/:companyId/notes/:noteId
```
Účel: Zmazať poznámku firmy
Auth: Áno
```
---
### 📁 PROJECTS
#### GET /api/projects?search=query&companyId=uuid
```
Účel: Zoznam projektov
Query: search, companyId (both optional)
Auth: Áno
```
#### GET /api/projects/:projectId
```
Účel: Detail projektu
Auth: Áno
```
> Poznámka: Endpoint `/api/projects/:projectId/details` bol odstránený (nepoužíva ho FE).
#### POST /api/projects
```
Účel: Vytvoriť projekt
Body: { name*, description, companyId, status, startDate, endDate }
Auth: Áno
Validation: createProjectSchema
```
#### PATCH /api/projects/:projectId
```
Účel: Upraviť projekt
Body: (all optional)
Auth: Áno
Validation: updateProjectSchema
```
#### DELETE /api/projects/:projectId
```
Účel: Zmazať projekt
Auth: Áno
Efekt: CASCADE delete todos, notes, projectUsers
```
#### GET /api/projects/:projectId/notes
```
Účel: Poznámky projektu
Auth: Áno
```
#### POST /api/projects/:projectId/notes
```
Účel: Pridať poznámku k projektu
Body: { content*, reminderAt? }
Auth: Áno
```
#### PATCH /api/projects/:projectId/notes/:noteId
```
Účel: Upraviť poznámku projektu
Auth: Áno
```
#### DELETE /api/projects/:projectId/notes/:noteId
```
Účel: Zmazať poznámku projektu
Auth: Áno
```
#### GET /api/projects/:projectId/users
```
Účel: Členovia tímu projektu (many-to-many relationship)
Auth: Áno
Response: Array of {
id,
userId,
role,
addedBy,
addedAt,
user: { id, username, email, role }
}
Volá: project.service.getProjectUsers()
```
#### POST /api/projects/:projectId/users
```
Účel: Priradiť usera k projektu
Body: { userId*, role? }
Auth: Áno
Validation:
- userId musí byť UUID
- role je optional (text)
- User nesmie byť už assigned (UNIQUE constraint)
Response: Assignment with user details
Volá: project.service.assignUserToProject()
Errors:
- 404: Project alebo User nenájdený
- 409: User už je assigned k projektu
```
#### PATCH /api/projects/:projectId/users/:userId
```
Účel: Zmeniť rolu usera na projekte
Params: projectId (UUID), userId (UUID)
Body: { role? }
Auth: Áno
Poznámka: Momentálne sa NEVYUŽÍVA vo frontende (role field removed)
Volá: project.service.updateUserRoleOnProject()
```
#### DELETE /api/projects/:projectId/users/:userId
```
Účel: Odstrániť usera z projektu
Params: projectId (UUID), userId (UUID)
Auth: Áno
Response: { success: true, message: 'Používateľ bol odstránený z projektu' }
Volá: project.service.removeUserFromProject()
Errors:
- 404: User nie je assigned k projektu
```
---
### ✅ TODOS
#### GET /api/todos?search=&projectId=&companyId=&assignedTo=&status=
```
Účel: Zoznam úloh
Query: všetky parametre optional
Auth: Áno
```
#### GET /api/todos/:todoId
```
Účel: Detail todo
Auth: Áno
```
> Poznámka: Endpoints `/api/todos/my` a `/api/todos/:todoId/details` boli odstránené (nepoužíva ich FE).
#### POST /api/todos
```
Účel: Vytvoriť todo
Body: { title*, description, projectId, companyId, assignedTo, status, priority, dueDate }
Auth: Áno
Validation: createTodoSchema
```
#### PATCH /api/todos/:todoId
```
Účel: Upraviť todo
Body: (all optional)
Auth: Áno
Efekt: Ak status=completed → nastaví completedAt
```
#### DELETE /api/todos/:todoId
```
Účel: Zmazať todo
Auth: Áno
```
#### PATCH /api/todos/:todoId/toggle
```
Účel: Toggle completed status
Auth: Áno
Efekt: status 'completed' ↔ 'pending'
Poznámka: Používa sa vo frontende pre checkbox toggle
Response: Updated todo
```
---
### 📝 NOTES (Standalone)
**POZNÁMKA:** Standalone note routes sú odpojené z app.js a frontend ich nepoužíva.
Poznámky sa riešia iba cez nested routes (companies/:id/notes, projects/:id/notes).
#### GET /api/notes?search=&companyId=&projectId=&todoId=&contactId=
```
Účel: Zoznam všetkých poznámok
Poznámka: NEVYUŽÍVA SA
```
#### GET /api/notes/my-reminders
```
Účel: Moje pending reminders
Poznámka: NEVYUŽÍVA SA (mohlo by byť užitočné!)
```
#### GET /api/notes/:noteId
```
Účel: Detail poznámky
Poznámka: NEVYUŽÍVA SA
```
#### POST /api/notes
```
Účel: Vytvoriť standalone poznámku
Poznámka: NEVYUŽÍVA SA
```
#### PATCH /api/notes/:noteId
```
Účel: Upraviť poznámku
Poznámka: NEVYUŽÍVA SA
```
#### DELETE /api/notes/:noteId
```
Účel: Zmazať poznámku
Poznámka: NEVYUŽÍVA SA
```
#### POST /api/notes/:noteId/mark-reminder-sent
```
Účel: Označiť reminder ako odoslaný
Poznámka: NEVYUŽÍVA SA
```
---
### 👤 CONTACTS
#### GET /api/contacts?accountId=uuid
```
Účel: Zoznam kontaktov pre email account
Query: accountId (required)
Auth: Áno
```
#### GET /api/contacts/discover?accountId=&search=&limit=
```
Účel: Objaviť potenciálne kontakty z emailov
Query: accountId (optional, uses primary), search, limit
Auth: Áno
Volá: jmap.service.discoverContactsFromJMAP()
```
#### POST /api/contacts
```
Účel: Pridať kontakt
Body: { email*, name, notes, accountId }
Auth: Áno
Efekt: Sync emails from sender, reassign existing emails
Volá: jmap.service.syncEmailsFromSender()
```
#### PATCH /api/contacts/:contactId?accountId=uuid
```
Účel: Upraviť kontakt
Body: { name, notes }
Query: accountId (required)
Auth: Áno
```
#### DELETE /api/contacts/:contactId?accountId=uuid
```
Účel: Zmazať kontakt
Query: accountId (required)
Auth: Áno
Efekt: CASCADE delete emails
```
> Poznámka: Link/unlink company a create-company routes boli odstránené (FE ich nevolá).
---
### 📧 EMAIL ACCOUNTS
#### GET /api/email-accounts
```
Účel: Zoznam mojich email účtov
Auth: Áno
Response: Array of accounts (bez passwords!)
```
> Poznámka: Endpoints `/api/email-accounts/:id`, `/:id/password`, `/:id/status` boli odstránené (nepoužíva ich FE).
#### POST /api/email-accounts
```
Účel: Pripojiť email account
Body: { email*, emailPassword* }
Auth: Áno
Rate Limit: Áno
Efekt:
- Validate credentials (JMAP)
- Encrypt password (AES-256-GCM)
- Ak existuje → share (shared=true)
- Ak prvý → set primary
- Sync contacts
Volá: email.service, password.encryptPassword()
```
#### POST /api/email-accounts/:id/set-primary
```
Účel: Nastaviť ako primárny email
Auth: Áno
Efekt: Ostatné accounts → isPrimary = false
```
#### DELETE /api/email-accounts/:id
```
Účel: Odstrániť prístup k emailu
Auth: Áno
Rate Limit: Áno
Efekt:
- Ak posledný user → delete account + data
- Ak shared → iba unlink
```
---
### ✉️ EMAILS (CRM Email Management)
#### GET /api/emails?accountId=uuid
```
Účel: Zoznam emailov
Query: accountId (required)
Auth: Áno
Response: Iba emaily od pridaných kontaktov!
```
#### GET /api/emails/thread/:threadId?accountId=uuid
```
Účel: Email thread (konverzácia)
Auth: Áno
Response: Všetky emaily v threade, sorted by date
```
#### GET /api/emails/search?accountId=&query=
```
Účel: Search v uložených emailoch (DB)
Query: accountId, query (min 2 chars)
Auth: Áno
Limit: 50 results
```
#### GET /api/emails/search-jmap?accountId=&query=
```
Účel: Full-text search cez JMAP server
Query: accountId, query
Auth: Áno
Volá: jmap.service.searchEmailsJMAP()
Poznámka: Hľadá vo VŠETKÝCH emailoch, nie len v DB
```
#### GET /api/emails/unread-count?accountId=uuid
```
Účel: Počet neprečítaných emailov
Query: accountId (required)
Auth: Áno
Response: { totalUnread, byAccount: [...] }
```
#### POST /api/emails/sync?accountId=uuid
```
Účel: Synchronizovať najnovšie emaily z JMAP
Query: accountId (required)
Auth: Áno
Efekt: Fetch latest emails, store to DB
Volá: jmap.service.syncEmails()
```
#### POST /api/emails/thread/:threadId/read?accountId=uuid
```
Účel: Označiť thread ako prečítaný
Auth: Áno
Efekt: UPDATE emails SET isRead = true + sync JMAP
```
#### POST /api/emails/contact/:contactId/read?accountId=uuid
```
Účel: Označiť všetky emaily kontaktu ako prečítané
Auth: Áno
```
> Poznámka: Endpoints `/api/emails/contact/:contactId` a `/api/emails/:jmapId/read` boli odstránené (FE ich nevolá).
#### POST /api/emails/reply
```
Účel: Odpovedať na email / poslať nový
Body: { to*, subject*, body*, inReplyTo?, threadId? }
Auth: Áno
Efekt: Send via JMAP, store to DB
Volá: jmap.service.sendEmail()
```
---
### ⏱️ TIME TRACKING
#### POST /api/time-tracking/start
```
Účel: Spustiť nový časovač
Body: { projectId?, todoId?, companyId?, description? }
Auth: Áno
Response: { entry with isRunning: true }
Efekt:
- Automaticky zastaví predchádzajúci bežiaci časovač (ak existuje)
- Vytvorí nový time entry s startTime = NOW()
```
#### POST /api/time-tracking/:entryId/stop
```
Účel: Zastaviť bežiaci časovač
Params: entryId (UUID)
Body: { projectId?, todoId?, companyId?, description? }
Auth: Áno
Response: { entry with endTime, duration, isRunning: false }
Efekt:
- Nastaví endTime = NOW()
- Vypočíta duration v minútach
- Optional: update projektId/todoId/companyId/description
```
#### GET /api/time-tracking/running
```
Účel: Získať aktuálny bežiaci časovač
Auth: Áno
Response: { entry } alebo null
```
#### GET /api/time-tracking?projectId=&todoId=&companyId=&startDate=&endDate=
```
Účel: Zoznam všetkých time entries s filtrami
Query: projectId, todoId, companyId, startDate, endDate (all optional)
Auth: Áno
Response: Array of time entries
Order: DESC by startTime
```
#### GET /api/time-tracking/month/:year/:month
```
Účel: Mesačný prehľad time entries
Params: year (YYYY), month (1-12)
Query: userId (optional, admin only ak je zadaný, načítava sa daný používateľ)
Auth: Áno
Response: Array of entries pre daný mesiac
```
#### POST /api/time-tracking/month/:year/:month/generate
```
Účel: Vygenerovať mesačný timesheet (Excel XLSX)
Params: year (YYYY), month (1-12)
Query: userId (optional, admin only - generate pre iného usera)
Auth: Áno
Body: {} (bez payloadu; posiela sa prázdny objekt)
Response: {
timesheet: { id, fileName, filePath, ... },
filePath,
entriesCount,
totalMinutes,
totalHours
}
Efekt:
- Vytvorí Excel súbor s time entries pre daný mesiac
- Obsahuje: denné záznamy, projekty, todos, descrip, duration
- Summary: daily totals + overall total
- Uloží do: uploads/timesheets/{userId}/{year}/{month}/
- INSERT INTO timesheets (isGenerated = true)
Volá: time-tracking.service.generateMonthlyTimesheet()
Admin feature: Admin môže generovať timesheet pre iného usera (query param userId)
```
#### GET /api/time-tracking/stats/monthly/:year/:month
```
Účel: Mesačné štatistiky
Params: year (YYYY), month (1-12)
Query: userId (optional, admin only ak je zadaný, načítava sa daný používateľ)
Auth: Áno
Response: {
totalMinutes,
totalHours,
remainingMinutes,
daysWorked,
averagePerDay,
entriesCount,
byProject: { projectId: minutes },
byCompany: { companyId: minutes }
}
```
#### GET /api/time-tracking/:entryId
```
Účel: Detail time entry
Params: entryId (UUID)
Auth: Áno
Response: Single time entry
```
#### GET /api/time-tracking/:entryId/details
```
Účel: Detail time entry s reláciami
Params: entryId (UUID)
Auth: Áno
Response: { ...entry, project, todo, company }
```
#### PATCH /api/time-tracking/:entryId
```
Účel: Upraviť time entry
Params: entryId (UUID)
Body: { startTime?, endTime?, projectId?, todoId?, companyId?, description? }
Auth: Áno
Validation:
- Entry nesmie byť isRunning (nemožno upraviť bežiaci časovač)
- User musí byť owner
- Pri zmene času sa prepočíta duration
Response: Updated entry with isEdited: true
```
#### DELETE /api/time-tracking/:entryId
```
Účel: Zmazať time entry
Params: entryId (UUID)
Auth: Áno
Validation:
- Entry nesmie byť isRunning
- User musí byť owner
```
---
### 📊 TIMESHEETS
#### POST /api/timesheets/upload
```
Účel: Upload timesheet file (manuálne nahraný PDF/Excel)
Content-Type: multipart/form-data
Form: file (PDF/Excel), year (YYYY), month (1-12)
Auth: Áno
Validation:
- Max 10MB
- Allowed types: PDF, Excel (xlsx, xls)
Efekt:
- Uloží file do: uploads/timesheets/{userId}/{year}/{month}/
- Generate unique filename: {name}-{timestamp}-{random}.ext
- INSERT INTO timesheets (isGenerated = false)
Response: { timesheet object }
```
#### GET /api/timesheets/my?year=YYYY&month=M
```
Účel: Moje timesheets (uploaded + generated)
Query: year, month (both optional)
Auth: Áno
Response: { timesheets: [...], count }
Order: DESC by uploadedAt
```
#### GET /api/timesheets/all?userId=uuid&year=YYYY&month=M
```
Účel: Všetky timesheets všetkých userov (admin)
Query: userId, year, month (all optional)
Auth: Áno (admin only)
Response: { timesheets: [...with user info...], count }
Includes: userId, username, firstName, lastName (LEFT JOIN users)
Order: DESC by uploadedAt
```
#### GET /api/timesheets/:timesheetId/download
```
Účel: Stiahnuť timesheet file
Auth: Áno
Permissions: Owner OR admin
Response: File download (res.download)
Errors:
- 404: Timesheet nenájdený alebo súbor neexistuje
- 403: Nemáte oprávnenie (nie vlastník ani admin)
```
#### DELETE /api/timesheets/:timesheetId
```
Účel: Zmazať timesheet
Auth: Áno
Permissions: Owner OR admin
Efekt:
- Delete file from filesystem (fs.unlink)
- DELETE FROM timesheets
- Continue even if file deletion fails (log error)
Errors:
- 404: Timesheet nenájdený
- 403: Nemáte oprávnenie
```
**POZNÁMKA:**
- Timesheets môžu byť **uploaded** (manuálne PDF/Excel) alebo **generated** (auto Excel z time entries)
- Field `isGenerated` rozlišuje typ: `true` = auto-generated, `false` = manually uploaded
- Obe typy sa ukladajú do rovnakej tabuľky `timesheets` a rovnakého adresára
- Generated timesheets sa vytvárajú cez `POST /api/time-tracking/month/:year/:month/generate`
---
## VZŤAHY MEDZI SLUŽBAMI
### Call Graph (kto volá koho)
```
AUTH FLOW:
auth.controller
→ auth.service
→ emailAccountService.createEmailAccount()
→ email.service.validateJmapCredentials()
→ password.encryptPassword()
→ contact.service.syncContactsForAccount()
→ jmap.service.discoverContactsFromJMAP()
→ password.hashPassword()
→ jwt.generateTokenPair()
CONTACT CREATION:
contact.controller
→ contact.service.addContact()
→ jmap.service.syncEmailsFromSender()
→ jmapRequest() → JMAP Server
→ INSERT emails to DB
COMPANY FROM CONTACT:
contact.controller
→ contact.service.createCompanyFromContact()
→ company.service.createCompany()
→ UPDATE contact.companyId
PROJECT TEAM:
project.controller
→ project.service.assignUserToProject()
→ Validate user exists (users table)
→ INSERT projectUsers
EMAIL SEND:
email.controller
→ jmap.service.sendEmail()
→ jmapRequest() → JMAP Server
→ INSERT email to DB (sent)
TIME TRACKING:
time-tracking.controller
→ time-tracking.service.startTimeEntry()
→ Check running entry (auto-stop if exists)
→ Validate project/todo/company
→ INSERT timeEntries
→ time-tracking.service.stopTimeEntry()
→ Calculate duration
→ UPDATE timeEntries (set endTime, isRunning = false)
→ time-tracking.service.getMonthlyStats()
→ Aggregate statistics by project/company
AUDIT:
Rôzne controllers
→ audit.service.logAuditEvent()
→ INSERT auditLogs
```
### Service Dependencies
**Tier 1 (No dependencies):**
- `password.js` (util)
- `jwt.js` (util)
- `logger.js` (util)
- `errors.js` (util)
**Tier 2 (Only utils):**
- `email.service` → (iba HTTP call)
- `audit.service` → (iba DB)
**Tier 3 (Utils + Tier 2):**
- `jmap.service` → database, HTTP
- `company.service` → database, errors
- `todo.service` → database, errors
- `note.service` → database, errors
- `time-tracking.service` → database, errors
**Tier 4 (Multiple services):**
- `contact.service` → company.service, jmap.service
- `emailAccountService` → email.service, password, contact.service
- `project.service` → database, errors
**Tier 5 (Highest level):**
- `auth.service` → emailAccountService, password, jwt
---
## POZNÁMKY
### Bezpečnosť
- **User passwords:** bcrypt (12 rounds, one-way hash)
- **Email passwords:** AES-256-GCM (reversible, pre JMAP login)
- **JWT tokens:** HS256, httpOnly cookies
- **Rate limiting:** Login, password change, email operations
### Performance
- **Database indexy:** Na všetkých foreign keys
- **Eager loading:** `getWithRelations()` metódy použiť iba ak treba všetko
- **Pagination:** Momentálne nie je, odporúčam pridať na lists
### Maintenance
- **Error handling:** Centralizované cez `formatErrorResponse()`
- **Logging:** Štruktúrované cez `logger`
- **Audit trail:** Všetky kritické akcie logované
### Možné zlepšenia
1. Pagination na všetkých list endpointoch
2. WebSocket pre real-time notifications (time tracking updates, email sync)
3. Background jobs pre email sync (Bull/Redis)
4. Cache layer (Redis) pre často čítané dáta
5. API versioning (/api/v1/)
6. GraphQL ako alternatíva k REST
7. Implement standalone Notes UI alebo vymazať routes
8. Time tracking export do CSV/Excel pre reporting
9. Team time tracking dashboard (admin view všetkých userov)
---
## ⚠️ TECHNICKÉ POZNÁMKY A GOTCHAS
### 1. Drizzle ORM - Join Query Pattern
**PROBLÉM:** Drizzle ORM nepodporuje nested object syntax v `.select()`
```javascript
// ❌ NEFUNGUJE - TypeError: Cannot convert undefined or null to object
const wrong = await db
.select({
id: projectUsers.id,
user: {
id: users.id,
username: users.username,
}
})
.from(projectUsers)
.leftJoin(users, eq(projectUsers.userId, users.id));
// ✅ SPRÁVNE RIEŠENIE
const rawResults = await db
.select() // Bez parametrov!
.from(projectUsers)
.leftJoin(users, eq(projectUsers.userId, users.id))
.where(eq(projectUsers.projectId, projectId))
.orderBy(desc(projectUsers.addedAt));
// Následne mapovať výsledky:
const assignedUsers = rawResults.map((row) => ({
id: row.project_users.id, // snake_case table name
userId: row.project_users.userId,
role: row.project_users.role,
user: row.users ? { // null check pre LEFT JOIN
id: row.users.id,
username: row.users.username,
email: row.users.email,
} : null,
}));
```
**Kde sa to používa:**
- `project.service.js`: `getProjectUsers()`, `assignUserToProject()`, `updateUserRoleOnProject()`, `getProjectWithRelations()`
- Všade kde robíme LEFT JOIN s users, companies, projects, atď.
---
### 2. Todo Status - Enum vs Boolean
**PROBLÉM:** Frontend pôvodne checkoval `todo.completed` (boolean), ale databáza má `status` enum.
**Databázová schéma:**
```javascript
status: pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled'])
```
**SPRÁVNE použitie vo frontende:**
```javascript
// ✅ SPRÁVNE
const isCompleted = todo.status === 'completed';
// ❌ NESPRÁVNE - toto pole neexistuje!
const isCompleted = todo.completed;
```
**Toggle endpoint:**
```javascript
// Backend: /api/todos/:todoId/toggle
// Toggleuje medzi: 'completed' ↔ 'pending'
const newStatus = todo.status === 'completed' ? 'pending' : 'completed';
```
**Kde sa to používa:**
- Frontend: `TodoItem.jsx`, `TodosPage.jsx`, `ProjectNotesModal.jsx`
- Backend: `todo.service.js` - `toggleTodoComplete()`
---
### 3. Node.js Module Caching
**PROBLÉM:** Po zmene kódu server niekedy beží stále starý kód, aj keď nodemon reštartoval.
**PRÍČINY:**
- Viacero nodemon procesov naraz (v separátnych termináloch)
- ESM module caching
- Neukončené background procesy
**RIEŠENIE:**
```bash
# Zabij všetky node/nodemon procesy
ps aux | grep -E "node|nodemon" | grep -v grep | awk '{print $2}' | xargs kill -9
# Alebo len na porte 5000
lsof -i:5000 | grep LISTEN | awk '{print $2}' | xargs kill -9
# Clear cache a reštart
rm -rf node_modules/.cache
npm run dev
```
**PREVENCIA:**
- Nespúšťať server v background mode počas developmentu
- Používať iba jeden terminál pre server
- Check procesy: `ps aux | grep node`
---
### 4. Frontend API Response Parsing
**PROBLÉM:** API môže vracať rôzne formáty odpovede.
**Admin API - Get All Users:**
```javascript
// Backend vracia:
{ users: [...], count: 10 }
// Frontend musí extrahovať:
const usersList = Array.isArray(data) ? data : (data?.users || [])
```
**Project API - Get Team Members:**
```javascript
// Backend vracia priamo array:
[{ id, userId, user: {...} }]
// Frontend očakáva array:
setTeamMembers(data || [])
```
**BEST PRACTICE:**
- Vždy checkuj `Array.isArray()`
- Fallback na prázdne array: `data || []`
- Console.log response v dev mode pre debug
---
### 5. Database Constraints
**projectUsers table - UNIQUE constraint:**
```javascript
unique('project_user_unique').on(table.projectId, table.userId)
```
**Význam:**
- User môže byť assigned k projektu **iba raz**
- Duplicate assignment → `409 ConflictError`
**Cascade deletes:**
```javascript
onDelete: 'cascade' // Ak sa zmaže project/user → zmaže sa assignment
```
**Kde sa používa:**
- Projects: CASCADE delete na todos, notes, timesheets, projectUsers
- Companies: CASCADE delete na projects (a všetko pod nimi)
- Email accounts: CASCADE delete na contacts, emails
---
### 6. Authentication Middleware
**Každá route (okrem /api/auth/login) vyžaduje authentication:**
```javascript
router.use(authenticate); // Global middleware na route file
```
**JWT token sa číta z:**
1. httpOnly cookie `accessToken` (preferované)
2. Authorization header `Bearer <token>` (fallback)
**User info v request:**
```javascript
req.userId // UUID
req.username // String
req.userRole // 'admin' | 'member'
```
---
### 7. Time Tracking - Auto-stop Behavior
**PROBLÉM:** User môže mať spustený iba jeden časovač naraz.
**RIEŠENIE - Automatický stop:**
```javascript
// Pri štarte nového časovača
if (existingRunning) {
// Automaticky zastaví predchádzajúci časovač
const endTime = new Date();
const duration = Math.round((endTime - startTime) / 60000); // minúty
await db.update(timeEntries)
.set({ endTime, duration, isRunning: false })
.where(eq(timeEntries.id, existingRunning.id));
}
```
**Validačné pravidlá:**
```javascript
// ✅ POVOLENÉ
- Spustiť nový časovač (auto-stop predchádzajúceho)
- Upraviť zastavený časovač
- Zmazať zastavený časovač
// ❌ ZAKÁZANÉ
- Upraviť bežiaci časovač (musí sa najprv zastaviť)
- Zmazať bežiaci časovač (musí sa najprv zastaviť)
- Zastaviť časovač iného usera
```
**Duration calculation:**
- Ukladá sa v **minútach** (Math.round)
- Počíta sa z rozdelu: `(endTime - startTime) / 60000`
- Frontend zobrazuje: hodiny + minúty (napr. "2h 35m")
**isEdited flag:**
- Automaticky nastavený pri manuálnej úprave
- Indikuje, že čas bol zmenený používateľom (nie auto-tracked)
**Kde sa používa:**
- `time-tracking.service.js`: `startTimeEntry()`, `updateTimeEntry()`
- Frontend: Time tracking komponenty s real-time countdown
---
## 🔍 DEBUGGING TIPS
### 1. Server beží starý kód
```bash
# Check running processes
ps aux | grep node
# Kill all and restart
pkill -9 node && npm run dev
```
### 2. Database query debugging
```javascript
// Drizzle query debugging
const result = await db.select()...
console.log('[DEBUG] Raw DB result:', JSON.stringify(result, null, 2));
```
### 3. Frontend API debugging
```javascript
// V API function
console.log('[DEBUG] API Response:', data);
console.log('[DEBUG] Extracted users:', usersList);
```
### 4. JMAP email issues
```javascript
// Check JMAP credentials
const valid = await validateJmapCredentials(email, password);
console.log('[DEBUG] JMAP validation:', valid);
```
---
**Vytvorené:** 2025-11-21
**Posledná aktualizácia:** 2025-11-25
**Autor:** CRM Server Team
---
## CHANGELOG
### 2025-11-25 - Cleanup + Timesheet Service
- Presunutá biznis logika timesheetov do `services/timesheet.service.js`, controller ostáva tenký.
- Odstránené nevyužité routes (FE): auth link-email/skip-email/me, company/project/todo details, contacts link/unlink/create-company, email-account detail/password/status, emails contact listing + PATCH read, standalone notes odpojené z app.js.
- Dokumentácia zosúladená s aktuálnymi endpointmi.
### 2025-11-24 - Additions
**Pridané sekcie:**
1. **VALIDATORS** - Kompletná dokumentácia všetkých Zod schemas
- auth.validators.js (login, password, user creation)
- crm.validators.js (company, project, todo, note, time tracking)
- email-account.validators.js (JMAP accounts)
2. **SERVICES** - Doplnené chýbajúce metódy
- time-tracking.service.generateMonthlyTimesheet() - Excel XLSX generation
3. **CONTROLLERS** - Pridaný chýbajúci controller
- timesheet.controller.js - File upload/download (bez service layer)
4. **ROUTES** - Kompletný zoznam všetkých route files
- 11 route files s uvedením middleware requirements
5. **API ROUTES** - Doplnené chýbajúce endpointy
- POST /api/time-tracking/month/:year/:month/generate - Generate Excel timesheet
- GET /api/timesheets/my - Detail s filters (year, month)
- GET /api/timesheets/all - Admin endpoint s filters
- DELETE /api/timesheets/:timesheetId - Permission checks
6. **UTILS** - Zoznam všetkých utility files (boli už zdokumentované)
**Upresnenia:**
- Timesheet service NEEXISTUJE - logika priamo v controlleri
- isGenerated flag rozlišuje uploaded vs generated timesheets
- Admin môže generovať timesheet pre iného usera (query param userId)
- Empty string handling vo validátoroch: `.or(z.literal(''))` pattern
- Optional UUID preprocessing v time tracking schemas