# 📚 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: už 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 má "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 už 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 má 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 sú 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 má 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
