# CRM Server - Kompletná Dokumentácia ## Obsah 1. [Prehľad projektu](#1-prehľad-projektu) 2. [Štruktúra projektu](#2-štruktúra-projektu) 3. [Databázová schéma](#3-databázová-schéma) 4. [API Endpoints](#4-api-endpoints) 5. [Middlewares](#5-middlewares) 6. [Services](#6-services) 7. [Controllers](#7-controllers) 8. [Utility funkcie](#8-utility-funkcie) 9. [Validačné schémy](#9-validačné-schémy) 10. [Konfigurácia](#10-konfigurácia) 11. [Autentizácia a bezpečnosť](#11-autentizácia-a-bezpečnosť) 12. [Cron Jobs a Notifikácie](#12-cron-jobs-a-notifikácie) --- ## 1. Prehľad projektu CRM Server je backend aplikácia postavená na: - **Express.js** - Web framework - **Drizzle ORM** - Databázový ORM - **PostgreSQL** - Databáza - **JWT** - Autentizácia - **Zod** - Validácia - **JMAP** - Email integrácia ### Hlavné funkcie: - Multi-user CRM s role-based access control (admin/member) - Email integrácia cez JMAP API - Time tracking s generovaním timesheetov (XLSX) - Contact management s linkovaním na firmy - Company/Project management s teamami - Todo/Task management s priorítami - Audit logging všetkých akcií - Company reminders s due dates - Meeting scheduling --- ## 2. Štruktúra projektu ``` crm-server/ ├── src/ │ ├── config/ │ │ └── database.js # Drizzle ORM + PostgreSQL konfigurácia │ │ │ ├── db/ │ │ ├── schema.js # Kompletná databázová schéma │ │ └── seeds/ │ │ ├── admin.seed.js # Seed pre admin usera │ │ └── testuser.seed.js # Seed pre testovacieho usera │ │ │ ├── controllers/ │ │ ├── auth.controller.js │ │ ├── admin.controller.js │ │ ├── contact.controller.js │ │ ├── personal-contact.controller.js │ │ ├── company.controller.js │ │ ├── company-reminder.controller.js │ │ ├── project.controller.js │ │ ├── todo.controller.js │ │ ├── time-tracking.controller.js │ │ ├── timesheet.controller.js │ │ ├── meeting.controller.js │ │ ├── note.controller.js │ │ ├── crm-email.controller.js │ │ ├── email-account.controller.js │ │ └── audit.controller.js │ │ │ ├── services/ │ │ ├── auth.service.js │ │ ├── admin.service.js │ │ ├── contact.service.js │ │ ├── personal-contact.service.js │ │ ├── company.service.js │ │ ├── company-reminder.service.js │ │ ├── company-email.service.js │ │ ├── project.service.js │ │ ├── todo.service.js │ │ ├── time-tracking.service.js │ │ ├── timesheet.service.js │ │ ├── meeting.service.js │ │ ├── note.service.js │ │ ├── crm-email.service.js │ │ ├── email-account.service.js │ │ ├── email.service.js │ │ ├── audit.service.js │ │ ├── status.service.js │ │ └── jmap/ │ │ ├── index.js # Hlavný JMAP export │ │ ├── client.js # JMAP klient │ │ ├── config.js # JMAP konfigurácia │ │ ├── discovery.js # JMAP discovery │ │ ├── operations.js # JMAP operácie │ │ ├── search.js # JMAP vyhľadávanie │ │ └── sync.js # JMAP synchronizácia │ │ │ ├── cron/ │ │ ├── index.js # Hlavný cron entry point │ │ └── calendar/ │ │ ├── index.js # Kalendárny cron scheduler │ │ ├── email-template.js # HTML email šablóna │ │ └── event-notifier.js # Logika notifikácií │ │ │ ├── routes/ │ │ ├── auth.routes.js │ │ ├── admin.routes.js │ │ ├── contact.routes.js │ │ ├── personal-contact.routes.js │ │ ├── company.routes.js │ │ ├── project.routes.js │ │ ├── todo.routes.js │ │ ├── time-tracking.routes.js │ │ ├── timesheet.routes.js │ │ ├── meeting.routes.js │ │ ├── note.routes.js │ │ ├── crm-email.routes.js │ │ ├── email-account.routes.js │ │ └── audit.routes.js │ │ │ ├── middlewares/ │ │ ├── auth/ │ │ │ ├── authMiddleware.js # JWT overenie │ │ │ ├── roleMiddleware.js # Role overenie │ │ │ └── resourceAccessMiddleware.js # Resource access control │ │ ├── security/ │ │ │ ├── rateLimiter.js # Rate limiting │ │ │ ├── validateInput.js # Input validácia │ │ │ └── requireAccountId.js # Email account check │ │ └── global/ │ │ ├── errorHandler.js # Error handling │ │ ├── validateBody.js # Body validation │ │ └── notFound.js # 404 handler │ │ │ ├── utils/ │ │ ├── errors.js # Custom error classes │ │ ├── jwt.js # JWT management │ │ ├── password.js # Password & encryption │ │ └── logger.js # Logger utility │ │ │ ├── validators/ │ │ ├── auth.validators.js # Auth schémy (Zod) │ │ ├── crm.validators.js # CRM schémy (Zod) │ │ └── email-account.validators.js │ │ │ ├── scripts/ │ │ ├── seed-admin.js │ │ ├── fresh-database.js │ │ └── fix-wrong-contact-associations.js │ │ │ ├── app.js # Express aplikácia │ └── index.js # Entry point │ ├── uploads/ # Nahrané súbory (timesheets) ├── __tests__/ # Testy ├── package.json ├── drizzle.config.js ├── Dockerfile └── .env ``` --- ## 3. Databázová schéma ### Enums ```sql -- Role používateľa CREATE TYPE role AS ENUM ('admin', 'member'); -- Status projektu CREATE TYPE project_status AS ENUM ('active', 'completed', 'on_hold', 'cancelled'); -- Status úlohy CREATE TYPE todo_status AS ENUM ('pending', 'in_progress', 'completed', 'cancelled'); -- Priorita úlohy CREATE TYPE todo_priority AS ENUM ('low', 'medium', 'high', 'urgent'); ``` ### Tabuľky #### users Používatelia systému. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | username | VARCHAR(50) | Unikátne užívateľské meno | | firstName | VARCHAR(100) | Krstné meno | | lastName | VARCHAR(100) | Priezvisko | | password | TEXT | Hash hesla | | tempPassword | TEXT | Dočasné heslo | | changedPassword | BOOLEAN | Či si zmenil heslo | | role | ENUM | 'admin' alebo 'member' | | lastLogin | TIMESTAMP | Posledné prihlásenie | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### email_accounts Zdieľané email účty. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | email | VARCHAR(255) | Email adresa | | emailPassword | TEXT | Šifrované heslo (AES-256-GCM) | | jmapAccountId | TEXT | JMAP account ID | | isActive | BOOLEAN | Či je účet aktívny | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### user_email_accounts M2M väzba: users ↔ email_accounts | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | userId | UUID | FK → users.id | | emailAccountId | UUID | FK → email_accounts.id | | isPrimary | BOOLEAN | Či je primárny účet | | addedAt | TIMESTAMP | Dátum pridania | #### companies Firmy/spoločnosti. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | name | VARCHAR(255) | Názov firmy | | description | TEXT | Popis | | address | TEXT | Adresa | | city | VARCHAR(100) | Mesto | | country | VARCHAR(100) | Krajina | | phone | VARCHAR(50) | Telefón | | email | VARCHAR(255) | Email | | website | VARCHAR(255) | Web stránka | | isActive | BOOLEAN | Či je aktívna | | createdBy | UUID | FK → users.id | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### company_users M2M väzba: companies ↔ users (tím firmy) | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | companyId | UUID | FK → companies.id (CASCADE) | | userId | UUID | FK → users.id (CASCADE) | | role | TEXT | Rola v tíme | | addedBy | UUID | FK → users.id (SET NULL) | | addedAt | TIMESTAMP | Dátum pridania | **Unique constraint:** (companyId, userId) #### company_remind Pripomienky firmy. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | companyId | UUID | FK → companies.id (CASCADE) | | description | TEXT | Popis pripomienky | | dueDate | TIMESTAMP | Termín | | isChecked | BOOLEAN | Či je splnená | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### projects Projekty. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | name | VARCHAR(255) | Názov projektu | | description | TEXT | Popis | | companyId | UUID | FK → companies.id (SET NULL) | | status | ENUM | Status projektu | | startDate | DATE | Dátum začiatku | | endDate | DATE | Dátum konca | | createdBy | UUID | FK → users.id | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### project_users M2M väzba: projects ↔ users (tím projektu) | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | projectId | UUID | FK → projects.id (CASCADE) | | userId | UUID | FK → users.id (CASCADE) | | role | TEXT | Rola v projekte | | addedBy | UUID | FK → users.id (SET NULL) | | addedAt | TIMESTAMP | Dátum pridania | **Unique constraint:** (projectId, userId) #### todos Úlohy/tasky. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | title | VARCHAR(255) | Názov úlohy | | description | TEXT | Popis | | projectId | UUID | FK → projects.id (SET NULL) | | companyId | UUID | FK → companies.id (SET NULL) | | status | ENUM | Status úlohy | | priority | ENUM | Priorita | | dueDate | TIMESTAMP | Termín | | completedAt | TIMESTAMP | Dátum dokončenia | | createdBy | UUID | FK → users.id | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### todo_users M2M väzba: todos ↔ users (priradení k úlohe) | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | todoId | UUID | FK → todos.id (CASCADE) | | userId | UUID | FK → users.id (CASCADE) | | assignedBy | UUID | FK → users.id (SET NULL) | | assignedAt | TIMESTAMP | Dátum priradenia | **Unique constraint:** (todoId, userId) #### contacts Kontakty (patriace email účtu). | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | emailAccountId | UUID | FK → email_accounts.id (CASCADE) | | companyId | UUID | FK → companies.id (SET NULL) | | email | VARCHAR(255) | Email kontaktu | | name | VARCHAR(255) | Meno kontaktu | | notes | TEXT | Poznámky | | addedBy | UUID | FK → users.id | | addedAt | TIMESTAMP | Dátum pridania | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### personal_contacts Osobné kontakty používateľa. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | userId | UUID | FK → users.id (CASCADE) | | firstName | VARCHAR(100) | Krstné meno | | lastName | VARCHAR(100) | Priezvisko | | phone | VARCHAR(50) | Telefón | | email | VARCHAR(255) | Email | | secondaryEmail | VARCHAR(255) | Sekundárny email | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### emails Uložené emaily z JMAP. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | emailAccountId | UUID | FK → email_accounts.id (CASCADE) | | contactId | UUID | FK → contacts.id (SET NULL) | | companyId | UUID | FK → companies.id (SET NULL) | | jmapId | TEXT | JMAP ID emailu | | messageId | TEXT | Message-ID header | | threadId | TEXT | JMAP thread ID | | inReplyTo | TEXT | In-Reply-To header | | from | JSONB | Odosielateľ | | to | JSONB | Príjemcovia | | subject | TEXT | Predmet | | body | TEXT | Telo emailu | | isRead | BOOLEAN | Či je prečítaný | | sentByUserId | UUID | FK → users.id (ak odoslané) | | date | TIMESTAMP | Dátum emailu | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### notes Poznámky (pripojené k rôznym entitám). | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | title | VARCHAR(255) | Titulok | | content | TEXT | Obsah poznámky | | companyId | UUID | FK → companies.id (SET NULL) | | projectId | UUID | FK → projects.id (SET NULL) | | todoId | UUID | FK → todos.id (SET NULL) | | contactId | UUID | FK → contacts.id (SET NULL) | | reminderAt | TIMESTAMP | Čas pripomienky | | reminderSent | BOOLEAN | Či bola pripomienka odoslaná | | createdBy | UUID | FK → users.id | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### time_entries Sledovanie času. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | userId | UUID | FK → users.id (CASCADE) | | projectId | UUID | FK → projects.id (SET NULL) | | todoId | UUID | FK → todos.id (SET NULL) | | companyId | UUID | FK → companies.id (SET NULL) | | startTime | TIMESTAMP | Čas začiatku | | endTime | TIMESTAMP | Čas konca | | duration | INTEGER | Trvanie v minútach | | description | TEXT | Popis práce | | isRunning | BOOLEAN | Či práve beží | | isEdited | BOOLEAN | Či bol upravený | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### timesheets Nahrané timesheets. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | userId | UUID | FK → users.id (CASCADE) | | projectId | UUID | FK → projects.id (SET NULL) | | fileName | VARCHAR(255) | Názov súboru | | filePath | TEXT | Cesta k súboru | | fileType | VARCHAR(10) | 'pdf' alebo 'xlsx' | | fileSize | INTEGER | Veľkosť v bytoch | | year | INTEGER | Rok | | month | INTEGER | Mesiac | | isGenerated | BOOLEAN | Či bol generovaný | | uploadedAt | TIMESTAMP | Dátum nahratia | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### meetings Stretnutia (iba admin). | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | title | VARCHAR(255) | Názov stretnutia | | description | TEXT | Popis | | start | TIMESTAMP | Čas začiatku | | end | TIMESTAMP | Čas konca | | createdBy | UUID | FK → users.id | | createdAt | TIMESTAMP | Dátum vytvorenia | | updatedAt | TIMESTAMP | Dátum aktualizácie | #### audit_logs Audit trail všetkých akcií. | Stĺpec | Typ | Popis | |--------|-----|-------| | id | UUID | Primárny kľúč | | userId | UUID | FK → users.id (SET NULL) | | action | VARCHAR(100) | Typ akcie | | resource | VARCHAR(100) | Typ resource | | resourceId | UUID | ID resource | | oldValue | JSONB | Stará hodnota | | newValue | JSONB | Nová hodnota | | ipAddress | VARCHAR(45) | IP adresa | | userAgent | TEXT | User agent | | success | BOOLEAN | Či bola úspešná | | errorMessage | TEXT | Chybová správa | | createdAt | TIMESTAMP | Dátum vytvorenia | --- ## 4. API Endpoints ### Formát Response **Success:** ```json { "success": true, "data": { }, "count": 10, "message": "Akcia úspešná" } ``` **Error:** ```json { "success": false, "error": { "message": "Chybová správa", "statusCode": 400, "details": [ { "field": "username", "message": "Povinné pole" } ] } } ``` ### Authentication (`/api/auth`) | Metóda | Endpoint | Popis | Auth | Rate Limit | |--------|----------|-------|------|------------| | POST | `/login` | Prihlásenie | Nie | 5/15min | | POST | `/set-password` | Nastavenie hesla | Áno | - | | POST | `/logout` | Odhlásenie | Áno | - | | GET | `/session` | Aktuálna session | Áno | - | ### Admin (`/api/admin`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/users` | Zoznam userov | Áno | Všetci | | POST | `/users` | Vytvoriť usera | Áno | Admin | | GET | `/users/:userId` | Detail usera | Áno | Admin | | PATCH | `/users/:userId/role` | Zmeniť rolu | Áno | Admin | | DELETE | `/users/:userId` | Zmazať usera | Áno | Admin | | GET | `/server-status` | Server status | Áno | Admin | | POST | `/trigger-notifications` | Manuálne spustiť notifikácie | Áno | Admin | ### Companies (`/api/companies`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam firiem | Áno | Všetci* | | GET | `/:companyId` | Detail firmy | Áno | Access | | POST | `/` | Vytvoriť firmu | Áno | Admin | | PATCH | `/:companyId` | Upraviť firmu | Áno | Admin | | DELETE | `/:companyId` | Zmazať firmu | Áno | Admin | | GET | `/reminders/summary` | Súhrn pripomienok | Áno | Všetci* | | GET | `/reminders/counts` | Počty pripomienok | Áno | Všetci* | | GET | `/reminders/upcoming` | Budúce pripomienky | Áno | Všetci* | | GET | `/email-unread` | Neprečítané emaily | Áno | Všetci | | GET | `/:companyId/email-threads` | Email vlákna | Áno | Access | **Company Notes:** | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/:companyId/notes` | Poznámky firmy | Áno | Access | | POST | `/:companyId/notes` | Pridať poznámku | Áno | Admin | | PATCH | `/:companyId/notes/:noteId` | Upraviť poznámku | Áno | Admin | | DELETE | `/:companyId/notes/:noteId` | Zmazať poznámku | Áno | Admin | **Company Reminders:** | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/:companyId/reminders` | Pripomienky firmy | Áno | Access | | POST | `/:companyId/reminders` | Vytvoriť | Áno | Admin | | PATCH | `/:companyId/reminders/:reminderId` | Upraviť | Áno | Admin | | DELETE | `/:companyId/reminders/:reminderId` | Zmazať | Áno | Admin | **Company Users (Team):** | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/:companyId/users` | Členovia tímu | Áno | Access | | POST | `/:companyId/users` | Pridať člena | Áno | Admin | | PATCH | `/:companyId/users/:userId` | Upraviť rolu | Áno | Admin | | DELETE | `/:companyId/users/:userId` | Odstrániť člena | Áno | Admin | *\* Member vidí len firmy kde je priradený* ### Projects (`/api/projects`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam projektov | Áno | Všetci* | | GET | `/:projectId` | Detail projektu | Áno | Access | | POST | `/` | Vytvoriť projekt | Áno | Admin | | PATCH | `/:projectId` | Upraviť projekt | Áno | Admin | | DELETE | `/:projectId` | Zmazať projekt | Áno | Admin | **Project Notes:** | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/:projectId/notes` | Poznámky projektu | Áno | Access | | POST | `/:projectId/notes` | Pridať poznámku | Áno | Admin | | PATCH | `/:projectId/notes/:noteId` | Upraviť | Áno | Admin | | DELETE | `/:projectId/notes/:noteId` | Zmazať | Áno | Admin | **Project Users (Team):** | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/:projectId/users` | Členovia projektu | Áno | Access | | POST | `/:projectId/users` | Pridať člena | Áno | Admin | | PATCH | `/:projectId/users/:userId` | Upraviť rolu | Áno | Admin | | DELETE | `/:projectId/users/:userId` | Odstrániť člena | Áno | Admin | *\* Member vidí len projekty kde je priradený alebo projekty firiem kde je priradený* ### Todos (`/api/todos`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam úloh | Áno | Všetci* | | GET | `/:todoId` | Detail úlohy | Áno | Access | | POST | `/` | Vytvoriť úlohu | Áno | Admin | | PATCH | `/:todoId` | Upraviť úlohu | Áno | Admin | | DELETE | `/:todoId` | Zmazať úlohu | Áno | Admin | | PATCH | `/:todoId/toggle` | Prepnúť stav | Áno | Access | *\* Member vidí len úlohy kde je priradený* **Query parametre pre GET /:** - `search` - Vyhľadávanie v title/description - `projectId` - Filter podľa projektu - `companyId` - Filter podľa firmy - `assignedTo` - Filter podľa priradeného usera - `status` - Filter podľa statusu (pending/in_progress/completed/cancelled) - `completed` - true/false (alternatíva k status) - `priority` - Filter podľa priority (low/medium/high/urgent) ### Contacts (`/api/contacts`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam kontaktov | Áno | Všetci | | GET | `/discover` | Objaviť z JMAP | Áno | Všetci | | POST | `/` | Pridať kontakt | Áno | Všetci | | PATCH | `/:contactId` | Upraviť kontakt | Áno | Všetci | | POST | `/:contactId/link-company` | Pripojiť firmu | Áno | Všetci | | POST | `/:contactId/unlink-company` | Odpojiť firmu | Áno | Všetci | | POST | `/:contactId/create-company` | Vytvoriť firmu | Áno | Všetci | | DELETE | `/:contactId` | Zmazať kontakt | Áno | Všetci | ### Personal Contacts (`/api/personal-contacts`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Moje osobné kontakty | Áno | Všetci | | POST | `/` | Vytvoriť | Áno | Všetci | | PUT | `/:contactId` | Upraviť | Áno | Všetci | | DELETE | `/:contactId` | Zmazať | Áno | Všetci | ### Time Tracking (`/api/time-tracking`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | POST | `/start` | Spustiť tracking | Áno | Všetci | | POST | `/:entryId/stop` | Zastaviť tracking | Áno | Všetci | | GET | `/running` | Bežiaci entry | Áno | Všetci | | GET | `/running-all` | Všetky bežiace | Áno | Všetci | | GET | `/` | Zoznam entries | Áno | Všetci | | GET | `/month/:year/:month` | Mesačné entries | Áno | Všetci | | POST | `/month/:year/:month/generate` | Generovať XLSX | Áno | Všetci | | GET | `/stats/monthly/:year/:month` | Mesačné štatistiky | Áno | Všetci | | GET | `/:entryId` | Detail entry | Áno | Všetci | | GET | `/:entryId/details` | Entry s reláciami | Áno | Všetci | | PATCH | `/:entryId` | Upraviť entry | Áno | Všetci | | DELETE | `/:entryId` | Zmazať entry | Áno | Všetci | ### Timesheets (`/api/timesheets`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | POST | `/upload` | Nahrať timesheet | Áno | Všetci | | GET | `/my` | Moje timesheets | Áno | Všetci | | GET | `/all` | Všetky timesheets | Áno | Admin | | GET | `/:timesheetId/download` | Stiahnuť súbor | Áno | Všetci | | DELETE | `/:timesheetId` | Zmazať | Áno | Všetci | ### Meetings (`/api/meetings`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam meetingov | Áno | Všetci | | GET | `/:meetingId` | Detail meetingu | Áno | Všetci | | POST | `/` | Vytvoriť meeting | Áno | Admin | | PUT | `/:meetingId` | Upraviť meeting | Áno | Admin | | DELETE | `/:meetingId` | Zmazať meeting | Áno | Admin | **Query parametre pre GET /:** - `year` - Rok - `month` - Mesiac ### Notes (`/api/notes`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam poznámok | Áno | Všetci | | GET | `/my-reminders` | Moje pripomienky | Áno | Všetci | | GET | `/:noteId` | Detail poznámky | Áno | Všetci | | POST | `/` | Vytvoriť poznámku | Áno | Všetci | | PATCH | `/:noteId` | Upraviť poznámku | Áno | Všetci | | DELETE | `/:noteId` | Zmazať poznámku | Áno | Všetci | | POST | `/:noteId/mark-reminder-sent` | Označiť odoslanú | Áno | Všetci | ### CRM Emails (`/api/emails`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Zoznam emailov | Áno | Všetci | | GET | `/search` | Vyhľadávanie v DB | Áno | Všetci | | GET | `/search-jmap` | JMAP full-text | Áno | Všetci | | GET | `/unread-count` | Počet neprečítaných | Áno | Všetci | | POST | `/sync` | Synchronizovať z JMAP | Áno | Všetci | | GET | `/thread/:threadId` | Email vlákno | Áno | Všetci | | POST | `/thread/:threadId/read` | Označiť prečítané | Áno | Všetci | | POST | `/contact/:contactId/read` | Označiť od kontaktu | Áno | Všetci | | POST | `/reply` | Odpovedať na email | Áno | Všetci | **Väčšina vyžaduje `accountId` v query parametroch.** ### Email Accounts (`/api/email-accounts`) | Metóda | Endpoint | Popis | Auth | Role | Rate Limit | |--------|----------|-------|------|------|------------| | GET | `/` | Moje email účty | Áno | Všetci | - | | POST | `/` | Vytvoriť účet | Áno | Všetci | Sensitive | | POST | `/:id/set-primary` | Nastaviť primárny | Áno | Všetci | - | | DELETE | `/:id` | Zmazať účet | Áno | Všetci | Sensitive | ### Audit Logs (`/api/audit-logs`) | Metóda | Endpoint | Popis | Auth | Role | |--------|----------|-------|------|------| | GET | `/` | Nedávne audit logy | Áno | Všetci | ### Health Check | Metóda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/health` | Health check | Nie | | GET | `/` | API info | Nie | --- ## 5. Middlewares ### Auth Middlewares (`/middlewares/auth/`) #### authMiddleware.js ```javascript authenticate(req, res, next) ``` - Overí JWT token z Authorization header alebo cookies - Načíta aktuálne user data z DB - Pridá `req.user` a `req.userId` - Vracia 401 ak token chýba/je neplatný/expiroval #### roleMiddleware.js ```javascript requireRole(...allowedRoles) ``` - Overí či má user jednu z povolených rolí - Vracia 403 ak nemá oprávnenie ```javascript requireAdmin ``` - Skratka pre `requireRole('admin')` ```javascript requireOwnerOrAdmin(getResourceUserId) ``` - Admin má vždy prístup - Inak overí ownership cez callback funkciu #### resourceAccessMiddleware.js ```javascript checkResourceAccess(resourceType, paramName) ``` - Univerzálny middleware pre kontrolu prístupu k resources - Admin má prístup vždy - Member len ak je priradený k resource ```javascript checkCompanyAccess // = checkResourceAccess('company', 'companyId') checkProjectAccess // = checkResourceAccess('project', 'projectId') checkTodoAccess // = checkResourceAccess('todo', 'todoId') ``` ```javascript getAccessibleResourceIds(resourceType, userId) ``` - Vráti zoznam resource IDs ku ktorým má user prístup - Použité v service vrstvách pre filtrovanie ```javascript hasAccessToResource(resourceType, userId, resourceId) ``` - Skontroluje či user má prístup k danému resource ```javascript canAccessResource(resourceType, userId, resourceId, userRole) ``` - Kombinácia role check + access check **Podporované resource typy:** | Resource Type | Junction Table | Resource ID Column | |--------------|----------------|-------------------| | company | company_users | companyId | | project | project_users | projectId | | todo | todo_users | todoId | ### Security Middlewares (`/middlewares/security/`) #### rateLimiter.js ```javascript loginRateLimiter ``` - Login endpoint: 5 pokusov za 15 minút ```javascript apiRateLimiter ``` - Všeobecné API: 1000 (dev) / 100 (prod) za 15 minút ```javascript sensitiveOperationLimiter ``` - Citlivé operácie: 50 (dev) / 10 (prod) za 15 minút #### validateInput.js ```javascript validateBody(schema) ``` - Validuje request body podľa Zod schémy ```javascript validateQuery(schema) ``` - Validuje query parametre ```javascript validateParams(schema) ``` - Validuje URL parametre Všetky vracajú 400 s detailmi chýb: ```json { "success": false, "error": { "message": "Validačná chyba", "statusCode": 400, "details": [ { "field": "email", "message": "Neplatný email" } ] } } ``` #### requireAccountId.js ```javascript requireAccountId(req, res, next) ``` - Kontroluje či má user `accountId` v query - Potrebné pre email operácie ### Global Middlewares (`/middlewares/global/`) #### errorHandler.js ```javascript errorHandler(err, req, res, next) ``` - Globálny error handler (posledný middleware) - Formátuje error response - Loguje chyby - Vracia príslušný status code #### validateBody.js ```javascript validateBodyMiddleware(req, res, next) ``` - Overí či je request body valid JSON #### notFound.js ```javascript notFoundHandler(req, res) ``` - Handler pre 404 (neexistujúce routes) --- ## 6. Services ### auth.service.js | Funkcia | Popis | |---------|-------| | `login(username, password, ip, userAgent)` | Prihlásenie s temp/permanent heslom | | `setPassword(userId, newPassword)` | Nastavenie nového hesla | | `logout(userId)` | Odhlásenie (audit log) | | `getUserById(userId)` | Získať usera podľa ID | ### admin.service.js | Funkcia | Popis | |---------|-------| | `getAllUsers()` | Zoznam všetkých userov | | `getUserById(userId)` | Detail usera | | `createUser(data)` | Vytvoriť usera s temp heslom | | `updateUserRole(userId, role)` | Zmeniť rolu usera | | `deleteUser(userId)` | Zmazať usera | ### company.service.js | Funkcia | Popis | |---------|-------| | `getAllCompanies(searchTerm, userId, userRole)` | Zoznam firiem (filtrované pre membera) | | `getCompanyById(companyId)` | Detail firmy | | `createCompany(data, userId)` | Vytvoriť firmu | | `updateCompany(companyId, data)` | Upraviť firmu | | `deleteCompany(companyId)` | Zmazať firmu | | `getCompanyUsers(companyId)` | Členovia tímu | | `addUserToCompany(companyId, userId, role, addedBy)` | Pridať člena | | `updateUserRoleInCompany(companyId, userId, role)` | Upraviť rolu | | `removeUserFromCompany(companyId, userId)` | Odstrániť člena | ### company-reminder.service.js | Funkcia | Popis | |---------|-------| | `getRemindersByCompanyId(companyId)` | Pripomienky firmy | | `createReminder(companyId, data)` | Vytvoriť pripomienku | | `updateReminder(reminderId, data)` | Upraviť pripomienku | | `deleteReminder(reminderId)` | Zmazať pripomienku | | `getReminderSummary(userId, userRole)` | Súhrn pripomienok (filtrované) | | `getReminderCountsByCompany(userId, userRole)` | Počty podľa firmy (filtrované) | | `getUpcomingReminders(limit, userId, userRole)` | Budúce pripomienky (filtrované) | ### project.service.js | Funkcia | Popis | |---------|-------| | `getAllProjects(searchTerm, companyId, userId, userRole)` | Zoznam projektov (filtrované) | | `getProjectById(projectId)` | Detail projektu | | `createProject(data, userId)` | Vytvoriť projekt | | `updateProject(projectId, data)` | Upraviť projekt | | `deleteProject(projectId)` | Zmazať projekt | | `getProjectUsers(projectId)` | Členovia projektu | | `assignUserToProject(projectId, userId, role, addedBy)` | Pridať člena | | `updateProjectUserRole(projectId, userId, role)` | Upraviť rolu | | `removeUserFromProject(projectId, userId)` | Odstrániť člena | ### todo.service.js | Funkcia | Popis | |---------|-------| | `getAllTodos(filters, userId, userRole)` | Zoznam úloh (filtrované) | | `getTodoById(todoId)` | Detail úlohy | | `createTodo(userId, data)` | Vytvoriť úlohu | | `updateTodo(todoId, data)` | Upraviť úlohu | | `deleteTodo(todoId)` | Zmazať úlohu | | `getTodoWithRelations(todoId)` | Úloha s reláciami | | `getTodosByProjectId(projectId)` | Úlohy projektu | | `getTodosByCompanyId(companyId)` | Úlohy firmy | | `getTodosByUserId(userId)` | Úlohy usera | ### contact.service.js | Funkcia | Popis | |---------|-------| | `getAllContacts(accountId, search, companyId)` | Zoznam kontaktov | | `getContactById(contactId)` | Detail kontaktu | | `createContact(data)` | Vytvoriť kontakt | | `updateContact(contactId, data)` | Upraviť kontakt | | `deleteContact(contactId)` | Zmazať kontakt | | `linkCompanyToContact(contactId, companyId)` | Pripojiť firmu | | `unlinkCompanyFromContact(contactId)` | Odpojiť firmu | ### personal-contact.service.js | Funkcia | Popis | |---------|-------| | `getPersonalContacts(userId)` | Osobné kontakty usera | | `createPersonalContact(userId, data)` | Vytvoriť | | `updatePersonalContact(contactId, userId, data)` | Upraviť | | `deletePersonalContact(contactId, userId)` | Zmazať | ### time-tracking.service.js | Funkcia | Popis | |---------|-------| | `startTimeEntry(userId, data)` | Spustiť tracking | | `stopTimeEntry(entryId, endTime)` | Zastaviť tracking | | `getRunningEntry(userId)` | Bežiaci entry usera | | `getAllRunningEntries()` | Všetky bežiace entries | | `getTimeEntries(filters)` | Zoznam entries | | `getTimeEntryById(entryId)` | Detail entry | | `getTimeEntryWithRelations(entryId)` | Entry s reláciami | | `updateTimeEntry(entryId, data)` | Upraviť entry | | `deleteTimeEntry(entryId)` | Zmazať entry | | `getMonthlyEntries(userId, year, month)` | Mesačné entries | | `getMonthlyStats(userId, year, month)` | Mesačné štatistiky | | `generateMonthlyTimesheet(userId, year, month)` | Generovať XLSX | ### timesheet.service.js | Funkcia | Popis | |---------|-------| | `uploadTimesheet(userId, file, year, month, projectId)` | Nahrať | | `getMyTimesheets(userId)` | Moje timesheets | | `getAllTimesheets()` | Všetky timesheets | | `getTimesheetById(timesheetId)` | Detail | | `deleteTimesheet(timesheetId, userId)` | Zmazať | ### meeting.service.js | Funkcia | Popis | |---------|-------| | `getMeetingsByMonth(year, month)` | Meetingy v mesiaci | | `getMeetingById(meetingId)` | Detail meetingu | | `createMeeting(data, userId)` | Vytvoriť | | `updateMeeting(meetingId, data)` | Upraviť | | `deleteMeeting(meetingId)` | Zmazať | ### note.service.js | Funkcia | Popis | |---------|-------| | `getAllNotes(filters)` | Zoznam poznámok | | `getNoteById(noteId)` | Detail poznámky | | `createNote(data, userId)` | Vytvoriť | | `updateNote(noteId, data)` | Upraviť | | `deleteNote(noteId)` | Zmazať | | `getMyReminders(userId)` | Moje pripomienky | | `markReminderSent(noteId)` | Označiť odoslanú | ### crm-email.service.js | Funkcia | Popis | |---------|-------| | `getEmails(accountId, filters)` | Zoznam emailov | | `searchEmails(accountId, query)` | Vyhľadávanie v DB | | `searchEmailsJmap(accountId, query)` | JMAP full-text | | `getUnreadCount(accountId)` | Počet neprečítaných | | `syncEmails(accountId)` | Synchronizovať z JMAP | | `getEmailThread(accountId, threadId)` | Email vlákno | | `markThreadAsRead(accountId, threadId)` | Označiť prečítané | | `markContactEmailsAsRead(accountId, contactId)` | Označiť od kontaktu | | `replyToEmail(accountId, userId, data)` | Odpovedať | ### email-account.service.js | Funkcia | Popis | |---------|-------| | `getUserEmailAccounts(userId)` | Email účty usera | | `createEmailAccount(userId, data)` | Vytvoriť účet | | `setPrimaryAccount(userId, accountId)` | Nastaviť primárny | | `deleteEmailAccount(userId, accountId)` | Zmazať účet | ### audit.service.js | Funkcia | Popis | |---------|-------| | `logAction(data)` | Zaznamenať akciu | | `logLogin(userId, success, ip, userAgent, error)` | Login log | | `logLogout(userId, ip, userAgent)` | Logout log | | `logPasswordChange(userId, ip, userAgent)` | Password change log | | `logTodoCreated(userId, todoId, title, ip, userAgent)` | Todo created log | | `logTodoDeleted(userId, todoId, title, ip, userAgent)` | Todo deleted log | | `logTodoCompleted(userId, todoId, title, ip, userAgent)` | Todo completed log | | `getRecentLogs(limit)` | Nedávne logy | ### status.service.js | Funkcia | Popis | |---------|-------| | `getServerStatus()` | CPU, RAM, Disk, Network, Uptime | ### JMAP Services (`/services/jmap/`) | Súbor | Funkcie | |-------|---------| | `client.js` | JMAP HTTP klient | | `config.js` | JMAP konfigurácia | | `discovery.js` | JMAP discovery (.well-known) | | `operations.js` | Email/set, Mailbox/get | | `search.js` | Email/query s full-text | | `sync.js` | Synchronizácia emailov | --- ## 7. Controllers Každý controller: 1. Extrahuje dáta z `req` (params, query, body, user) 2. Volá príslušný service 3. Formátuje a vracia response 4. Zachytáva a propaguje chyby cez `next(error)` ### Príklad controller funkcie: ```javascript export const getAllCompanies = async (req, res, next) => { try { const { search } = req.query; const userId = req.user?.id; const userRole = req.user?.role; const companies = await companyService.getAllCompanies(search, userId, userRole); res.status(200).json({ success: true, count: companies.length, data: companies, }); } catch (error) { next(error); } }; ``` --- ## 8. Utility funkcie ### errors.js ```javascript class AppError extends Error { constructor(message, statusCode, details = null) } class ValidationError extends AppError { constructor(message, details = []) // 400 } class BadRequestError extends AppError { constructor(message) // 400 } class AuthenticationError extends AppError { constructor(message) // 401 } class ForbiddenError extends AppError { constructor(message) // 403 } class NotFoundError extends AppError { constructor(message) // 404 } class ConflictError extends AppError { constructor(message) // 409 } class RateLimitError extends AppError { constructor(message) // 429 } formatErrorResponse(error, includeStack = false) ``` ### jwt.js ```javascript generateAccessToken(payload) // Expires: 1h generateRefreshToken(payload) // Expires: 7d verifyAccessToken(token) // Verify + decode verifyRefreshToken(token) // Verify + decode generateTokenPair(user) // Both tokens ``` ### password.js ```javascript hashPassword(password) // bcrypt, 12 rounds comparePassword(password, hash) // Verify password generateTempPassword(length = 12) // Random temp password generateVerificationToken() // UUID token encryptPassword(text) // AES-256-GCM encrypt decryptPassword(encryptedText) // AES-256-GCM decrypt ``` ### logger.js ```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 ``` --- ## 9. Validačné schémy ### auth.validators.js ```javascript loginSchema = z.object({ username: z.string().min(3).max(50), password: z.string().min(1) }) setPasswordSchema = z.object({ newPassword: z.string().min(8).max(100).regex(/[A-Z]/).regex(/[a-z]/).regex(/[0-9]/), confirmPassword: z.string() }).refine(data => data.newPassword === data.confirmPassword) createUserSchema = z.object({ username: z.string().min(3).max(50), email: z.string().email().optional(), firstName: z.string().min(1).max(100), lastName: z.string().min(1).max(100), role: z.enum(['admin', 'member']).default('member') }) changeRoleSchema = z.object({ role: z.enum(['admin', 'member']) }) ``` ### crm.validators.js ```javascript // Company createCompanySchema = z.object({ name: z.string().min(1).max(255), description: z.string().optional(), address: z.string().optional(), city: z.string().max(100).optional(), country: z.string().max(100).optional(), phone: z.string().max(50).optional(), email: z.string().email().optional(), website: z.string().url().optional() }) // Project createProjectSchema = z.object({ name: z.string().min(1).max(255), description: z.string().optional(), companyId: z.string().uuid().optional(), status: z.enum(['active', 'completed', 'on_hold', 'cancelled']).default('active'), startDate: z.string().optional(), endDate: z.string().optional() }) // Todo createTodoSchema = z.object({ title: z.string().min(1).max(255), description: z.string().optional(), projectId: z.string().uuid().optional(), companyId: z.string().uuid().optional(), assignedUserIds: z.array(z.string().uuid()).optional(), status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).default('pending'), priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'), dueDate: z.string().optional() }) // Time Entry startTimeEntrySchema = z.object({ projectId: z.string().uuid().optional(), todoId: z.string().uuid().optional(), companyId: z.string().uuid().optional(), description: z.string().optional() }) // Company Reminder createCompanyReminderSchema = z.object({ description: z.string().min(1), dueDate: z.string(), isChecked: z.boolean().default(false) }) ``` --- ## 10. Konfigurácia ### .env ```bash # Database DB_HOST=localhost DB_PORT=5432 DB_USER=admin DB_PASSWORD=heslo123 DB_NAME=crm # JWT JWT_SECRET=tajny_kluc_pre_access_token JWT_REFRESH_SECRET=tajny_kluc_pre_refresh_token JWT_EXPIRES_IN=1h JWT_REFRESH_EXPIRES_IN=7d # Encryption ENCRYPTION_SALT=salt_pre_enkrypciu_emailovych_hesiel BCRYPT_ROUNDS=12 # CORS CORS_ORIGIN=http://localhost:5173 # Notifikácie NOTIFICATION_TIME=07:00 # Čas odosielania (HH:mm) NOTIFICATION_SENDER_EMAIL=riso@slovensko.ai # Email odosielateľa (musí byť v DB) NOTIFICATION_TEST_MODE=false # true = cron beží každú minútu # Rate Limiting RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_LOGIN_MAX=5 RATE_LIMIT_MAX_REQUESTS=100 # Server NODE_ENV=development PORT=5000 ``` ### package.json scripts ```bash npm run dev # nodemon src/index.js npm run start # node src/index.js npm run test # Jest testy npm run db:generate # drizzle-kit generate npm run db:push # drizzle-kit push npm run db:studio # drizzle-kit studio npm run db:seed # Admin seed npm run db:seed:testuser # Test user seed ``` ### drizzle.config.js ```javascript export default { schema: './src/db/schema.js', out: './drizzle', dialect: 'postgresql', dbCredentials: { host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }, }; ``` --- ## 11. Autentizácia a bezpečnosť ### Autentizačný flow 1. **Login** - User zadá username + temp heslo 2. **Validácia** - Kontrola temp/permanent hesla 3. **Token generovanie** - Access token (1h) + Refresh token (7d) 4. **Cookies** - httpOnly, secure, sameSite=strict 5. **Password change** - Povinný v prvej prihláške 6. **Email setup** - Nastavenie email accounts ### Security features | Feature | Implementácia | |---------|---------------| | JWT tokeny | httpOnly cookies | | Rate limiting | express-rate-limit | | CORS | cors middleware | | XSS protection | xss-clean | | Security headers | Helmet.js | | Password hashing | bcrypt (12 rounds) | | Email password encryption | AES-256-GCM | | Audit logging | Všetky akcie | | RBAC | admin/member role | | Resource access control | Junction tables | ### Role-based access control | Akcia | Admin | Member | |-------|-------|--------| | Vytvoriť/upraviť/zmazať firmu | ✓ | ✗ | | Vytvoriť/upraviť/zmazať projekt | ✓ | ✗ | | Vytvoriť/upraviť/zmazať todo | ✓ | ✗ | | Toggle todo (splnené) | ✓ | ✓ (ak priradený) | | Zobraziť firmu | ✓ | ✓ (ak priradený) | | Zobraziť projekt | ✓ | ✓ (ak priradený) | | Zobraziť todo | ✓ | ✓ (ak priradený) | | Spravovať tím | ✓ | ✗ | | Server status | ✓ | ✗ | | Vytvoriť/upraviť/zmazať meeting | ✓ | ✗ | | Time tracking | ✓ | ✓ | | Kontakty | ✓ | ✓ | | Emaily | ✓ | ✓ | --- ## 12. Cron Jobs a Notifikácie ### Prehľad Systém obsahuje automatické cron joby pre odosielanie emailových notifikácií. Cron jobs sa spúšťajú automaticky pri štarte servera. ### Štruktúra ``` src/cron/ ├── index.js # Hlavný entry point - spúšťa všetky cron jobs └── calendar/ ├── index.js # Kalendárny cron scheduler ├── email-template.js # HTML email šablóna pre notifikácie └── event-notifier.js # Logika pre odosielanie notifikácií ``` ### Kalendárne notifikácie #### Ako to funguje 1. **Cron beží každý deň** o čase nastavenom v `NOTIFICATION_TIME` (default: 07:00) 2. **Vyhľadá eventy** ktoré začínajú **zajtra** (00:00 - 23:59) 3. **Získa priradených používateľov** cez `eventUsers` junction tabuľku 4. **Zistí primárny email** každého používateľa z `userEmailAccounts` 5. **Odošle HTML email** cez JMAP z účtu nastaveného v `NOTIFICATION_SENDER_EMAIL` #### Kedy sa notifikácie posielajú | Event začína | Notifikácia | |--------------|-------------| | Včera / dnes (minulosť) | ❌ Nie | | Zajtra | ✅ Áno | | Pozajtra a neskôr | ❌ Nie | #### Konfigurácia ```bash # .env NOTIFICATION_TIME=07:00 # Čas odosielania (formát HH:mm) NOTIFICATION_SENDER_EMAIL=riso@slovensko.ai # Email účet v databáze NOTIFICATION_TEST_MODE=false # true = cron beží každú minútu ``` **Dôležité:** `NOTIFICATION_SENDER_EMAIL` musí byť email účet uložený v tabuľke `email_accounts` s platným zašifrovaným heslom. #### Testovací mód Pre testovanie nastavte v `.env`: ```bash NOTIFICATION_TEST_MODE=true ``` Cron bude bežať **každú minútu** namiesto raz denne. Po dokončení testovania nastavte späť na `false`. #### Manuálne spustenie Admin môže manuálne spustiť notifikácie cez API: ```bash POST /api/admin/trigger-notifications Authorization: Bearer ``` **Response:** ```json { "success": true, "data": { "sent": 2, "failed": 0, "skipped": 1 }, "message": "Notifikácie odoslané: 2, neúspešné: 0, preskočené: 1" } ``` - `sent` - Počet úspešne odoslaných emailov - `failed` - Počet neúspešných odoslaní - `skipped` - Počet preskočených (napr. používateľ nemá nastavený email) ### Email šablóna Notifikačný email obsahuje: - **Header** s logom CRM - **Pozdrav** s menom používateľa - **Karta eventu** s: - Typ (Stretnutie / Udalosť) - farebne odlíšené - Názov eventu - Popis (ak existuje) - Dátum a čas - **Footer** s informáciou o automatickom generovaní Email je responzívny a zobrazuje sa správne v rôznych email klientoch. ### Logy Pri behu cron jobu sa zobrazujú logy: ``` [INFO] === Inicializujem cron jobs === [INFO] Nastavujem cron pre kalendárne notifikácie: 00 07 * * * (07:00 každý deň) [SUCCESS] Kalendárny notifikačný cron naplánovaný: 07:00 každý deň (Europe/Bratislava) [INFO] === Všetky cron jobs inicializované === # Pri spustení jobu: [INFO] Cron job spustený - kontrolujem zajtrajšie udalosti [INFO] === Spúšťam kontrolu zajtrajších udalostí === [INFO] Hľadám udalosti od 2025-12-16T00:00:00.000Z do 2025-12-16T23:59:59.999Z [INFO] Nájdených 2 priradení udalostí na zajtra [INFO] Unikátnych notifikácií na odoslanie: 2 [INFO] Odosielam notifikáciu pre admin (riso@slovensko.ai) - udalosť: Team meeting [SUCCESS] Email úspešne odoslaný na riso@slovensko.ai [INFO] === Hotovo: odoslaných 2, neúspešných 0, preskočených 0 === ``` ### Rozšírenie Pre pridanie nových cron jobs: 1. Vytvorte nový folder v `src/cron/` (napr. `reminders/`) 2. Implementujte logiku podobne ako v `calendar/` 3. Exportujte start funkciu 4. Importujte a zavolajte v `src/cron/index.js` ```javascript // src/cron/index.js import { startCalendarNotificationCron } from './calendar/index.js'; import { startRemindersCron } from './reminders/index.js'; // nový export const startAllCronJobs = () => { logger.info('=== Inicializujem cron jobs ==='); startCalendarNotificationCron(); startRemindersCron(); // nový logger.info('=== Všetky cron jobs inicializované ==='); }; ``` --- ## Záver Táto dokumentácia poskytuje kompletný prehľad CRM Server backendu. Pre ďalšie informácie kontaktujte administrátora projektu.