Files
crm-server/DOKUMENTACIA.md
2025-11-24 10:18:28 +01:00

1985 lines
47 KiB
Markdown

# 📚 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)
```
**Volá:**
- Databázu (timeEntries, projects, todos, companies)
- `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)
---
## CONTROLLERS
**Účel:** Spracovanie HTTP requestov, volanie services, vracanie responses
### Š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
### Š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
### 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/link-email
```
Účel: Pripojenie email účtu
Body: { email, emailPassword }
Auth: Áno
Volá: email.service, emailAccountService
```
#### POST /api/auth/skip-email
```
Účel: Preskočiť email setup
Auth: Á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 }
```
#### GET /api/auth/me
```
Účel: Profil aktuálneho usera
Auth: Áno
Response: { user with emailAccounts }
```
---
### 👥 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
```
#### GET /api/companies/:companyId/details
```
Účel: Firma s všetkými reláciami
Auth: Áno
Response: { ...company, projects: [], todos: [], notes: [] }
Poznámka: NEVYUŽÍVA SA vo frontende (robí sa 3 samostatné cally)
```
#### 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
```
#### GET /api/projects/:projectId/details
```
Účel: Projekt s reláciami
Auth: Áno
Response: { ...project, company, todos, notes, timesheets, assignedUsers }
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### 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/my?status=
```
Účel: Moje úlohy (assigned to current user)
Auth: Áno
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### GET /api/todos/:todoId
```
Účel: Detail todo
Auth: Áno
```
#### GET /api/todos/:todoId/details
```
Účel: Todo s reláciami
Auth: Áno
Response: { ...todo, project, company, assignedUser, notes }
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### 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:** Všetky standalone note routes sú **NEVYUŽITÉ** vo frontende.
Notes sa používajú 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
```
#### POST /api/contacts/:contactId/link-company?accountId=uuid
```
Účel: Linknúť firmu k kontaktu
Body: { companyId* }
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### POST /api/contacts/:contactId/unlink-company?accountId=uuid
```
Účel: Odlinkovať firmu od kontaktu
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### POST /api/contacts/:contactId/create-company?accountId=uuid
```
Účel: Vytvoriť firmu z kontaktu
Body: (optional) { name, email, phone, ... }
Poznámka: NEVYUŽÍVA SA vo frontende
Efekt: Vytvorí company, nastaví contact.companyId
```
---
### 📧 EMAIL ACCOUNTS
#### GET /api/email-accounts
```
Účel: Zoznam mojich email účtov
Auth: Áno
Response: Array of accounts (bez passwords!)
```
#### GET /api/email-accounts/:id
```
Účel: Detail email accountu
Auth: Áno
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### 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()
```
#### PATCH /api/email-accounts/:id/password
```
Účel: Zmeniť heslo k emailu
Body: { emailPassword* }
Auth: Áno
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### PATCH /api/email-accounts/:id/status
```
Účel: Aktivovať/deaktivovať email account
Body: { isActive* }
Auth: Áno
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### 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
```
#### GET /api/emails/contact/:contactId?accountId=uuid
```
Účel: Emaily od konkrétneho kontaktu
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### POST /api/emails/contact/:contactId/read?accountId=uuid
```
Účel: Označiť všetky emaily kontaktu ako prečítané
Auth: Áno
```
#### PATCH /api/emails/:jmapId/read?accountId=uuid
```
Účel: Označiť jeden email ako read/unread
Body: { isRead* }
Auth: Áno
Poznámka: NEVYUŽÍVA SA vo frontende
```
#### 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)
Auth: Áno
Response: Array of entries pre daný mesiac
```
#### GET /api/time-tracking/stats/monthly/:year/:month
```
Účel: Mesačné štatistiky
Params: year (YYYY), month (1-12)
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
Content-Type: multipart/form-data
Form: file (PDF/Excel), year (YYYY), month (1-12)
Auth: Áno
Validation: Max 10MB
```
#### GET /api/timesheets/my
```
Účel: Moje timesheets
Auth: Áno
```
#### GET /api/timesheets/all
```
Účel: Všetky timesheets (admin)
Auth: Áno (admin only)
```
#### GET /api/timesheets/:timesheetId/download
```
Účel: Stiahnuť timesheet file
Auth: Áno
Response: File download
```
#### DELETE /api/timesheets/:timesheetId
```
Účel: Zmazať timesheet
Auth: Áno
Efekt: Delete file + DB record
```
---
## 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-24
**Autor:** CRM Server Team
**Kontakt:** crm-server documentation