2220 lines
57 KiB
Markdown
2220 lines
57 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: 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
|