57 KiB
📚 CRM SERVER - KOMPLETNÁ DOKUMENTÁCIA
Vytvorené: 2025-11-21 Verzia: 1.1 Backend: Node.js + Express + Drizzle ORM + PostgreSQL Frontend: React
📑 OBSAH
- Architektúra Systému
- Services - Biznis Logika
- Controllers - Request Handling
- Routes - API Endpointy
- Utils - Pomocné Funkcie
- Kompletný Zoznam API
- 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-manyusers→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:
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:
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.NotFoundErrorutils/errors.ConflictError
3. contact.service.js
Účel: Správa email kontaktov (per email account)
Databáza: contacts, emails, companies
Metódy:
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:
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.NotFoundErrorutils/errors.ConflictError
Dôležité - Drizzle ORM Query Pattern:
// ✅ 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:
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:
// 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:
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:
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:
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:
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:
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.NotFoundErrorutils/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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
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:
- admin.controller.js - User management (admin only)
- auth.controller.js - Autentifikácia a onboarding
- company.controller.js - Firmy CRUD + nested notes
- contact.controller.js - Email kontakty
- crm-email.controller.js - Email management (read status, search)
- email-account.controller.js - JMAP účty
- note.controller.js - Standalone poznámky (nevyužité)
- project.controller.js - Projekty CRUD + nested notes + team management
- todo.controller.js - Úlohy CRUD
- time-tracking.controller.js - Sledovanie času
- timesheet.controller.js - Upload a download timesheetov (bez service!)
Štruktúra každého controllera:
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):
{
"success": true/false,
"data": { ... },
"count": 10, // optional (pre lists)
"message": "Success/Error message"
}
ROUTES
Účel: Definícia endpointov, middleware, validácia
Zoznam Route Files:
- admin.routes.js - User management (Auth + Admin role)
- auth.routes.js - Login, set password (Mixed public/protected)
- company.routes.js - Firmy + nested notes (Auth only)
- contact.routes.js - Kontakty (Auth only)
- crm-email.routes.js - Emaily (Auth only)
- email-account.routes.js - JMAP účty (Auth only)
- project.routes.js - Projekty + notes + team (Auth only)
- todo.routes.js - Úlohy (Auth only)
- time-tracking.routes.js - Time tracking (Auth only)
- timesheet.routes.js - Timesheets upload/download (Auth, admin for /all)
- note.routes.js - Standalone poznámky (odpojené z app.js, ponechané len ako archív)
Štruktúra route file:
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:
authenticate- JWT validáciavalidateParams- Path params validácia (Zod)validateBody- Request body validácia (Zod)controller.method- Controller handler
UTILS
Zoznam Utility Files:
- errors.js - Custom error classes + formatting
- jwt.js - JWT token generation and validation
- logger.js - Colored console logging
- password.js - Password hashing, encryption, generation
1. errors.js
Účel: Custom error classy a formatting
Error Classes:
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:
formatErrorResponse(error, includeStack)
→ Formátuje error pre API response
→ V dev mode: vracia stack trace
→ V prod mode: iba message
Použitie:
throw new NotFoundError('Company not found')
throw new ConflictError('Email already exists')
2. jwt.js
Účel: JWT token management
Funkcie:
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:
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:
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:
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:
// 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/detailsbol 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/detailsbol 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/mya/api/todos/:todoId/detailsboli 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/statusboli 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/:contactIda/api/emails/:jmapId/readboli 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
isGeneratedrozlišuje typ:true= auto-generated,false= manually uploaded - Obe typy sa ukladajú do rovnakej tabuľky
timesheetsa 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, HTTPcompany.service→ database, errorstodo.service→ database, errorsnote.service→ database, errorstime-tracking.service→ database, errors
Tier 4 (Multiple services):
contact.service→ company.service, jmap.serviceemailAccountService→ email.service, password, contact.serviceproject.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
- Pagination na všetkých list endpointoch
- WebSocket pre real-time notifications (time tracking updates, email sync)
- Background jobs pre email sync (Bull/Redis)
- Cache layer (Redis) pre často čítané dáta
- API versioning (/api/v1/)
- GraphQL ako alternatíva k REST
- Implement standalone Notes UI alebo vymazať routes
- Time tracking export do CSV/Excel pre reporting
- 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()
// ❌ 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:
status: pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled'])
SPRÁVNE použitie vo frontende:
// ✅ SPRÁVNE
const isCompleted = todo.status === 'completed';
// ❌ NESPRÁVNE - toto pole neexistuje!
const isCompleted = todo.completed;
Toggle endpoint:
// 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:
# 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:
// Backend vracia:
{ users: [...], count: 10 }
// Frontend musí extrahovať:
const usersList = Array.isArray(data) ? data : (data?.users || [])
Project API - Get Team Members:
// 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:
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:
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:
router.use(authenticate); // Global middleware na route file
JWT token sa číta z:
- httpOnly cookie
accessToken(preferované) - Authorization header
Bearer <token>(fallback)
User info v request:
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:
// 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á:
// ✅ 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
# Check running processes
ps aux | grep node
# Kill all and restart
pkill -9 node && npm run dev
2. Database query debugging
// Drizzle query debugging
const result = await db.select()...
console.log('[DEBUG] Raw DB result:', JSON.stringify(result, null, 2));
3. Frontend API debugging
// V API function
console.log('[DEBUG] API Response:', data);
console.log('[DEBUG] Extracted users:', usersList);
4. JMAP email issues
// 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:
-
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)
-
SERVICES - Doplnené chýbajúce metódy
- time-tracking.service.generateMonthlyTimesheet() - Excel XLSX generation
-
CONTROLLERS - Pridaný chýbajúci controller
- timesheet.controller.js - File upload/download (bez service layer)
-
ROUTES - Kompletný zoznam všetkých route files
- 11 route files s uvedením middleware requirements
-
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
-
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