diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md deleted file mode 100644 index 5ac13a4..0000000 --- a/DOCUMENTATION.md +++ /dev/null @@ -1,1550 +0,0 @@ -# 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. diff --git a/README.md b/README.md deleted file mode 100644 index 407b189..0000000 --- a/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# CRM Server – Architektúra a flow - -Tento dokument popisuje, ako backend funguje: aká je štruktúra kódu, akou cestou prechádza požiadavka, aké služby spolu komunikujú a kde sa riešia bezpečnostné a chybové scenáre. Všetky cesty v kóde sú písané v ES modules. - -## Základný stack -- **Express 4** (ESM) + **Drizzle ORM** (PostgreSQL) -- **JWT** pre prístupové/refresh tokeny (httpOnly cookies), **bcrypt** pre heslá -- **Zod** na validačné schémy, **helmet**, **cors**, **express-rate-limit** -- **JMAP** integrácia pre e-maily (Truemail) + AES-256-GCM šifrovanie hesiel k e-mail účtom - -## Štruktúra priečinkov (hlavné časti) -- `src/app.js` – skladá middleware pipeline a mountuje routes, pridáva notFound/error handler. -- `src/index.js` – spúšťací bod servera. -- `src/routes/` – deklarácie endpointov (1 súbor = 1 doména). Všetky používajú middlewares + volajú controller. -- `src/controllers/` – spracovanie requestu, volanie service vrstvy, odoslanie odpovedí. Chyby posielajú cez `next(err)`. -- `src/services/` – biznis logika a práca s DB/JMAP (bez Express závislosti). -- `src/middlewares/` – auth (JWT + role), security (rate limiting, Zod validateInput), global (validateBody pattern check, 404, error handler). -- `src/utils/` – `errors.js` (AppError a formátovanie), `jwt.js`, `password.js`, `logger.js`. -- `src/validators/` – Zod schémy pre vstupy. -- `src/db/` – Drizzle schéma a config. - -## Životný cyklus požiadavky (pipeline) -1) **Logovanie**: `morgan('dev')` (len stdout). -2) **Bezpečnostné hlavičky**: `helmet` s CSP (self + inline styles) a HSTS (preload, subdomains). -3) **CORS**: povolený pôvod z `CORS_ORIGIN` (default `http://localhost:5173`), credentials povolené. -4) **Body parsers**: `express.json` a `express.urlencoded` (limit 10 MB), `cookieParser` pre JWT v cookies. -5) **Global validateBody**: rýchly regex-detektor podozrivých payloadov (loguje a vráti 400 pri matches). -6) **Rate limiting**: `apiRateLimiter` na `/api/*` (100 req/15 min v production, 1000 v dev). Špecifické limitery na login a citlivé operácie sa aplikujú v routes. -7) **Routes**: `auth`, `admin`, `contacts`, `emails` (CRM), `email-accounts`, `timesheets`, `companies`, `projects`, `todos`, `time-tracking`, `notes`. -8) **404**: `notFound` middleware nastaví 404 a pošle ďalej ako Error. -9) **Global error handler**: `errorHandler` loguje, zvolí status (`err.statusCode` > `res.statusCode` ≥400 > 500) a formátuje pomocou `formatErrorResponse`. Ak už sú hlavičky poslané, púšťa chybu ďalej. - -## Validácia a bezpečnosť -- **Zod validácia**: `validateBody/validateQuery/validateParams` (v `middlewares/security/validateInput.js`) na úrovni routes; nahrádzajú `req.body/query/params` validovanými dátami. -- **Auth**: `authenticate` vytiahne JWT z Bearer header alebo cookie, overí cez `verifyAccessToken`, načíta usera (`auth.service.getUserById`) a uloží do `req.user` + `req.userId`. -- **Role**: `requireRole` / `requireAdmin` / `requireOwnerOrAdmin` (role middleware) na autorizáciu. -- **Rate limiting**: `loginRateLimiter` (default 5 pokusov/15 min, počíta len neúspechy), `sensitiveOperationLimiter` (10 prod / 50 dev za 15 min). -- **Šifrovanie**: `password.encryptPassword` používa AES-256-GCM (kľúč zo `JWT_SECRET` + `ENCRYPTION_SALT`). Heslá v DB sú bcrypt hashované. -- **Audit logy**: `audit.service` loguje login pokusy, zmeny hesla, role, tvorbu userov atď. do DB + konzoly. - -## Error handling (kde a ako) -- **Typy chýb**: `AppError` + podtriedy (ValidationError, AuthenticationError, ForbiddenError, NotFoundError, ConflictError, RateLimitError). -- **Formát odpovede**: `formatErrorResponse` vracia `{ success:false, error:{ message, statusCode, details?, stack? } }`. Stack iba v NODE_ENV=development. -- **Použitie**: Kontroléry nemajú lokálne try/catch formatovanie; pri chybe volajú `next(err)`. Auth middleware vracia 401 pri Auth chybách, inak púšťa ďalej do globálneho handlera. - -## Doménové moduly – kto koho volá - -### Autentifikácia (`routes/auth.routes.js`, `auth.controller.js`, `auth.service.js`) -- **/login**: `loginRateLimiter` → `validateBody(loginSchema)` → controller zavolá `authService.loginWithTempPassword` (porovná temp/permanent heslo, nastaví lastLogin, vygeneruje tokeny) → audit `logLoginAttempt` (success/fail) → nastaví httpOnly cookies + response. -- **/set-password**: `authenticate` → `sensitiveOperationLimiter` → `validateBody(setPasswordSchema)` → `authService.setNewPassword` (bcrypt, zmaže tempPassword) → audit `logPasswordChange`. -- **/logout**, **/session**: vyžadujú `authenticate`; logout vyčistí cookies, session vráti `req.user`. -- **Tokeny**: generované v `utils/jwt.js` (access + refresh); overenie hádže `AuthenticationError` pri expirácii/neplatnosti. - -### Admin (`routes/admin.routes.js`, `admin.controller.js`) -- `router.use(authenticate)` + `router.use(requireAdmin)` pre všetky admin-only akcie. -- CRUD nad používateľmi: create (generuje temp heslo + voliteľné linknutie email účtu), get/list, change role, delete. Používa `email-account.service` pri zakladaní účtu, loguje audit udalosti. - -### Email účty (`routes/email-account.routes.js`, `email-account.controller.js`, `email-account.service.js`) -- Každá akcia vyžaduje `authenticate`. -- **Create**: `sensitiveOperationLimiter` + Zod schéma → service overí JMAP credentials (`validateJmapCredentials` z `email.service.js`), šifruje heslo (AES-256-GCM), vytvorí účet a many-to-many link do `userEmailAccounts`. Ak účet existuje, vie ho len „nasdieliť“ po overení hesla. -- **Set primary**: pre konkrétneho používateľa; transakčne zruší ostatné `isPrimary` a aktivuje účet. -- **Delete**: odstráni link, a ak nikto iný účet nepoužíva, zmaže aj samotný účet. -- **Get**: vracia účty používateľa; špeciálna funkcia `getEmailAccountWithCredentials` dešifruje heslo na JMAP operácie. - -### CRM Emaily (`routes/crm-email.routes.js`, `crm-email.controller.js`, `crm-email.service.js`, `jmap.service.js`) -- Všetky endpointy za `authenticate`. -- **Listing/search**: DB filter + fulltext (subject/body/from), alebo JMAP full-text (`/search-jmap`). Filtrovanie podľa účtu, kontaktu, stavu prečítania. -- **Threads**: `/thread/:threadId` načíta konverzáciu; `/thread/:threadId/read` označí všetky maily v threade. -- **Sync**: `/sync` spustí fetch z JMAP pre daného používateľa/účet. -- **Mark contact read**: `/contact/:contactId/read` označí všetky maily od kontaktu ako prečítané. -- **Reply**: `/reply` cez JMAP; používa dešifrované heslo z email-account service. -- **Unread count**: `/unread-count` agreguje per účet. - -### Kontakty (`routes/contact.routes.js`, `contact.controller.js`, `contact.service.js`) -- Všetko za `authenticate`. -- **Get/Discover**: zoznam kontaktov (voliteľný filter `accountId`), discover číta unikátnych odosielateľov z JMAP. -- **Create**: validácia + uloženie, následne auto-sync všetkých emailov od tohto odosielateľa. -- **Update/Delete**: meno/poznámky, prípadne zmazanie; pri delete ostávajú emaily, len sa odpojí contact_id. -- **Link/Unlink company** a **create company from contact** využívajú company service. - -### Companies (`routes/company.routes.js`, `company.controller.js`, `company.service.js`, `company-email.service.js`, `company-reminder.service.js`) -- `authenticate` povinné. -- **CRUD firmy**, plus **email threads** pre firmu (agregácia emailov naprieč účtami používateľa). -- **Unread counts** per firma (agreguje emaily podľa kontaktov a účtov). -- **Notes** (vnořené /:companyId/notes) používajú `note.service`. -- **Reminders**: CRUD + summary/endpoints na prehľad (upcoming, counts, summary). Všetko cez `company-reminder.service`. - -### Projekty (`routes/project.routes.js`, `project.controller.js`, `project.service.js`) -- `authenticate` povinné. -- CRUD projektov, správa členov projektu (assign/update role/remove), projektové poznámky (vrátane `reminderAt`). - -### Todos (`routes/todo.routes.js`, `todo.controller.js`, `todo.service.js`) -- `authenticate`; CRUD + toggle completed. - -### Poznámky (`routes/note.routes.js`, `note.controller.js`, `note.service.js`) -- `authenticate`; všeobecné poznámky s filtrom na company/project/todo/contact; CRUD operácie. - -### Time Tracking (`routes/time-tracking.routes.js`, `time-tracking.controller.js`, `time-tracking.service.js`) -- `authenticate`; Zod validácia na start/stop/update. -- **Start/Stop**: vytvára/uzatvára bežiaci záznam (oprávnenie viazané na `req.userId`). -- **Bežiace položky**: `/running` (aktuálne pre usera), `/running-all` (všetkých userov – dashboard). -- **Listing/Filters**: všeobecný listing, mesačné výpisy a štatistiky, detail/relations. -- **Generate timesheet**: vytvorí XLSX výstup za mesiac (využíva `exceljs`). - -### Timesheets upload (`routes/timesheet.routes.js`, `timesheet.controller.js`, `timesheet.service.js`) -- `authenticate`; Multer s **memory storage** a limit 5 MB, whitelist MIME (PDF, XLSX, XLS). Admin môže nahrávať za iných, inak len za seba. Ukladá súbory do `uploads/timesheets`. - -### Admin (už popísané vyššie) - -### Audit (`audit.service.js`) -- Jednotné logovanie udalostí do DB + konzoly (tag `[AUDIT]`). Použité v auth flow a user managemente; možno rozšíriť na ďalšie služby. - -## Pomocné utility -- `utils/errors.js` – definícia AppError podtried + `formatErrorResponse`. -- `utils/jwt.js` – generovanie/verifikácia access/refresh tokenov. -- `utils/password.js` – bcrypt hash/compare, generovanie temp hesla, AES-256-GCM encrypt/decrypt pre email heslá. -- `utils/logger.js` – farebný stdout logger (info/success/warn/error/debug/audit). - -## Request → DB/JMAP tok (v skratke) -`Route` → `Zod middleware` (+ auth/role/rate-limit) → `Controller` → `Service` → `DB (Drizzle)` alebo `JMAP` → späť do controller → JSON response. Chyby: buď AppError (očakávané, neseká stack v prod), alebo neočakávané → global `errorHandler`. - -## Dôležité závislosti medzi modulmi -- **authMiddleware** závisí na `utils/jwt` a `auth.service` (pre user fetch). Bez prístupu k DB nie je možné overiť token. -- **crm-email.service** používa `jmap.service` + `email-account.service` (pre dešifrované heslo) + DB schémy. -- **contact.service** pri vytvorení kontaktu volá `crm-email.service` na sync emailov od odosielateľa. -- **company.controller** používa `company-email.service` (agregácia email threadov) a `note.service` / `company-reminder.service`. -- **time-tracking.service** používa Drizzle schémy `timeEntries`, `projects`, `todos`, `companies`, `users` na joiny a agregácie. -- **timesheet.controller**/routes využívajú Multer; uloženie súboru/metadata robí `timesheet.service`. - -## Ako rozširovať -- Nový endpoint: pridaj Zod schému do `validators`, zapoj `validateBody/Params/Query`, použi `authenticate`/`requireAdmin` podľa potreby, v controllery volaj service a pri chybe `next(err)`. -- Nová logika: implementuj v `services` (bez Express závislosti), AppError pri očakávaných stavoch. -- Error/response shape je centrálne daný `errorHandler` + `formatErrorResponse` – nechaj ho pracovať. - -## Environment a spustenie (stručne) -- Env vars: `PORT`, `CORS_ORIGIN`, `JWT_SECRET`, `JWT_REFRESH_SECRET`, `ENCRYPTION_SALT`, `RATE_LIMIT_*`, DB credentials atď. (pozri `.env.example` ak existuje / README bezpečnostný checklist). -- Skripty: `npm run dev` (nodemon), `npm start`, `npm test` (Jest), drizzle migrácie `db:generate/push/studio`. - -Tento README má slúžiť ako rýchla mapa: čo kde je, čo koho volá a kde hľadať validačné/bezpečnostné háky alebo biznis logiku. diff --git a/SQL_QUERIES.txt b/SQL_QUERIES.txt deleted file mode 100644 index ec0afae..0000000 --- a/SQL_QUERIES.txt +++ /dev/null @@ -1,140 +0,0 @@ -================================================================================ -KOMPLETNE SQL PRE COOLIFY - KOPIRUJ A PRILEP DO PSQL -================================================================================ - --------------------------------------------------------------------------------- -CAST 1: SCHEMA MIGRATION (spusti prvy) --------------------------------------------------------------------------------- - -DO $$ BEGIN CREATE TYPE "forma_kurzu_enum" AS ENUM('prezencne', 'online', 'hybridne'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -DO $$ BEGIN CREATE TYPE "stav_registracie_enum" AS ENUM('potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -DO $$ BEGIN CREATE TYPE "typ_prilohy_enum" AS ENUM('certifikat', 'faktura', 'prihlaska', 'doklad_o_platbe', 'ine'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -CREATE TABLE IF NOT EXISTS "chat_groups" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "name" text NOT NULL, "created_by_id" uuid REFERENCES "users"("id") ON DELETE SET NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "chat_group_members" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "group_id" uuid NOT NULL REFERENCES "chat_groups"("id") ON DELETE CASCADE, "user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, "joined_at" timestamp DEFAULT now() NOT NULL, "last_read_at" timestamp DEFAULT now() NOT NULL, CONSTRAINT "chat_group_member_unique" UNIQUE("group_id","user_id")); - -CREATE TABLE IF NOT EXISTS "group_messages" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "group_id" uuid NOT NULL REFERENCES "chat_groups"("id") ON DELETE CASCADE, "sender_id" uuid REFERENCES "users"("id") ON DELETE SET NULL, "content" text NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "push_subscriptions" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, "endpoint" text NOT NULL, "p256dh" text NOT NULL, "auth" text NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, CONSTRAINT "push_subscription_endpoint_unique" UNIQUE("user_id","endpoint")); - -CREATE TABLE IF NOT EXISTS "email_signatures" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL UNIQUE REFERENCES "users"("id") ON DELETE CASCADE, "full_name" text, "position" text, "phone" text, "email" text, "company_name" text, "website" text, "is_enabled" boolean DEFAULT true NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "services" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "name" text NOT NULL, "price" text NOT NULL, "description" text, "created_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "service_folders" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "name" text NOT NULL, "created_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "service_documents" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "folder_id" uuid NOT NULL REFERENCES "service_folders"("id") ON DELETE CASCADE, "file_name" text NOT NULL, "original_name" text NOT NULL, "file_path" text NOT NULL, "file_type" text NOT NULL, "file_size" integer NOT NULL, "description" text, "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, "uploaded_at" timestamp DEFAULT now() NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "company_documents" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "company_id" uuid NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE, "file_name" text NOT NULL, "original_name" text NOT NULL, "file_path" text NOT NULL, "file_type" text NOT NULL, "file_size" integer NOT NULL, "description" text, "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, "uploaded_at" timestamp DEFAULT now() NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "project_documents" ("id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "project_id" uuid NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE, "file_name" text NOT NULL, "original_name" text NOT NULL, "file_path" text NOT NULL, "file_type" text NOT NULL, "file_size" integer NOT NULL, "description" text, "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, "uploaded_at" timestamp DEFAULT now() NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "kurzy" ("id" serial PRIMARY KEY NOT NULL, "nazov" varchar(255) NOT NULL, "typ_kurzu" varchar(100) NOT NULL, "popis" text, "cena" numeric(10, 2) NOT NULL, "max_kapacita" integer, "aktivny" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "ucastnici" ("id" serial PRIMARY KEY NOT NULL, "titul" varchar(50), "meno" varchar(100) NOT NULL, "priezvisko" varchar(100) NOT NULL, "email" varchar(255) NOT NULL UNIQUE, "telefon" varchar(50), "firma" varchar(255), "mesto" varchar(100), "ulica" varchar(255), "psc" varchar(10), "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "registracie" ("id" serial PRIMARY KEY NOT NULL, "kurz_id" integer NOT NULL REFERENCES "kurzy"("id") ON DELETE CASCADE, "ucastnik_id" integer NOT NULL REFERENCES "ucastnici"("id") ON DELETE CASCADE, "datum_od" date, "datum_do" date, "forma_kurzu" "forma_kurzu_enum" DEFAULT 'prezencne' NOT NULL, "pocet_ucastnikov" integer DEFAULT 1 NOT NULL, "faktura_cislo" varchar(100), "faktura_vystavena" boolean DEFAULT false NOT NULL, "zaplatene" boolean DEFAULT false NOT NULL, "stav" "stav_registracie_enum" DEFAULT 'registrovany' NOT NULL, "poznamka" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL); - -CREATE TABLE IF NOT EXISTS "prilohy" ("id" serial PRIMARY KEY NOT NULL, "registracia_id" integer NOT NULL REFERENCES "registracie"("id") ON DELETE CASCADE, "nazov_suboru" varchar(255) NOT NULL, "typ_prilohy" "typ_prilohy_enum" DEFAULT 'ine' NOT NULL, "cesta_k_suboru" varchar(500) NOT NULL, "mime_type" varchar(100), "velkost_suboru" bigint, "popis" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL); - -CREATE UNIQUE INDEX IF NOT EXISTS "ucastnici_email_idx" ON "ucastnici" USING btree ("email"); -CREATE UNIQUE INDEX IF NOT EXISTS "registracie_kurz_ucastnik_idx" ON "registracie" USING btree ("kurz_id","ucastnik_id"); - -DO $$ BEGIN ALTER TABLE "todos" ADD COLUMN "completed_notified_at" timestamp; EXCEPTION WHEN duplicate_column THEN NULL; END $$; -ALTER TABLE "personal_contacts" ALTER COLUMN "phone" DROP NOT NULL; - --------------------------------------------------------------------------------- -CAST 2: AI KURZY DATA IMPORT (spusti po schema migration) --------------------------------------------------------------------------------- - -DELETE FROM prilohy; -DELETE FROM registracie; -DELETE FROM ucastnici; -DELETE FROM kurzy; - -ALTER SEQUENCE kurzy_id_seq RESTART WITH 1; -ALTER SEQUENCE ucastnici_id_seq RESTART WITH 1; -ALTER SEQUENCE registracie_id_seq RESTART WITH 1; - -INSERT INTO kurzy (nazov, typ_kurzu, cena, aktivny) VALUES ('AI 1+2 (2 dni) - 290€', 'AI', 290.00, true), ('AI 1 (1 deň) - 150€', 'AI', 150.00, true), ('AI 2 (1 deň) - 150€', 'AI', 150.00, true), ('AI v SEO (1 deň) - 150€', 'SEO', 150.00, true), ('AI I+II Marec 2026', 'AI', 290.00, true), ('AI I+II Apríl 2026', 'AI', 290.00, true); - -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, mesto, ulica, psc) VALUES (NULL, 'Martin', 'Sovák', 'info@energium.sk', '0918986172', 'energium sro', 'Bratislava', 'Topolcianska 5', '85105'), (NULL, 'Michal', 'Farkaš', 'michal.farkas83@gmail.com', '0911209122', 'SLOVWELD', 'Dunajska Lužná', 'Mandlova 30', '90042'), (NULL, 'Alena', 'Šranková', 'alena.srankova@gmail.com', '0917352580', NULL, 'Bratislava', 'Šándorova 1', '82103'), (NULL, 'Katarina', 'Tomaníková', 'k.tomanikova@riseday.net', '0948 070 611', 'Classica Shipping Limited', 'Bratislava', 'Keltska 104', '85110'), (NULL, 'Róbert', 'Brišák', 'robert.brisak@ss-nizna.sk', '0910583883', 'Spojená škola, Hattalova 471, 02743 Nižná', 'Nižná', 'Hattalova 471', '02743'), (NULL, 'Marián', 'Bača', 'baca.marian@gmail.com', '0907994126', NULL, 'Petrovany', '8', '08253'), ('Mgr. MBA', 'Nikola', 'Horáčková', 'nikolahorackova11@gmail.com', '0918482184', NULL, 'Zákopčie', 'Zákopčie stred 12', '023 11'), (NULL, 'Tomáš', 'Kupec', 'kupec.tom@gmail.com', '0911030190', 'Jamajka', 'Liptovská Sielnica', NULL, '032 23'), (NULL, 'Anton', 'Považský', 'anton.povazsky@example.com', NULL, NULL, NULL, NULL, NULL); - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026020' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'info@energium.sk'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'online', 1, true, true, 'registrovany', NULL FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'alena.srankova@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, true, 'registrovany', 'presunuta z oktobra, chce až január' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'k.tomanikova@riseday.net'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026019' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'robert.brisak@ss-nizna.sk'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, false, false, 'potencialny', 'vzdelávací poukaz' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'nikolahorackova11@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-02', '2026-02-02', 'online', 1, true, true, 'registrovany', 'Fa 2025 338, Súhlasil so zmeneným termínom' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1 (1 deň) - 150€' AND u.email = 'michal.farkas83@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-03', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'Fa Gablasova' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 2 (1 deň) - 150€' AND u.email = 'baca.marian@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', 'FA 2026021' FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO (1 deň) - 150€' AND u.email = 'kupec.tom@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', NULL FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO (1 deň) - 150€' AND u.email = 'anton.povazsky@example.com'; - -SELECT 'Kurzy:' as info, COUNT(*) as pocet FROM kurzy; -SELECT 'Ucastnici:' as info, COUNT(*) as pocet FROM ucastnici; -SELECT 'Registracie:' as info, COUNT(*) as pocet FROM registracie; - --------------------------------------------------------------------------------- -CAST 3: SERVICES DATA IMPORT (sluzby z firmy.slovensko.ai) --------------------------------------------------------------------------------- - -DELETE FROM service_documents; -DELETE FROM service_folders; -DELETE FROM services; - -INSERT INTO services (name, price, description) VALUES -('AI Hlasový Agent', 'od 149€/mesiac', 'Virtuálny telefónny operátor pre automatizáciu prichádzajúcich hovorov. Dostupný 24/7, spracováva rezervácie, poskytuje informácie, zbiera kontaktné údaje. Balíky: Silver (149€/mes, 500 min), Gold (399€/mes, 1500 min), VIP (899€/mes, 5000 min), Enterprise (individuálne).'), -('AI Server', 'od 5 000€', 'Výkonné servery optimalizované pre AI úlohy. Dáta zostávajú pod vašou kontrolou. Bronze (od 5000€, 24 jadier, 64GB RAM), Silver (od 15000€, 64 jadier, 256GB RAM), Gold (od 40000€, 128+ jadier, 512GB RAM), Platinum (od 100000€, 256+ jadier, 1TB RAM).'), -('AI Automatizácia procesov', 'individuálne', 'Zefektívnite firemné procesy pomocou AI riešení na mieru. Analýza workflow, návrh riešenia, vývoj prototypu, testovanie s reálnymi dátami, nasadenie a optimalizácia. Zníženie chýb, nákladov a zlepšenie výkonu.'), -('AI Chatbot', 'individuálne', 'Automatizovaná komunikácia so zákazníkmi pomocou ChatGPT a Dialogflow. Dostupný 24/7, integrácia s WhatsApp, Instagram a inými platformami. Analýza dokumentov, základné právne poradenstvo, administratívna automatizácia.'), -('AI Školenia', 'od 150€/osoba', 'Firemné školenia zamerané na praktické AI zručnosti. AI I: 150€ (1 deň), AI I+II: 290€ (2 dni). Možnosť prispôsobenia programu podľa odvetvia. Formát: prezenčne, online alebo u klienta.'), -('E-mailový AI Agent', 'individuálne', 'Inteligentný systém na automatizáciu spracovania e-mailov. Automatická analýza, generovanie odpovedí, kategorizácia, smerovanie a tvorba súhrnov. Podpora viacerých schránok s prispôsobiteľnými pravidlami automatizácie.'), -('E-shop (AI Hardware)', 'od 4 490€', 'Profesionálne grafické karty a servery optimalizované pre AI. Nvidia Tesla A100 40GB (od 4490€), A100 64GB (od 9900€), A100 80GB (od 12500€), RTX PRO 6000 Blackwell (12900€). Servery na vyžiadanie.'), -('AI Konzultácie', 'od 0€', 'Bezplatná 30-minútová konzultácia. Telefónna konzultácia: 0-80€/hod, Online/osobná: 70-80€/hod, On-site (do 50km vrátane): 80-100€/hod. AI poradenstvo, odporúčanie nástrojov, integrácia a pilotné nasadenie.'); - -SELECT 'Services imported:' as info, COUNT(*) as count FROM services; - --------------------------------------------------------------------------------- -CAST 4: CLEANUP (VOLITELNE - MAZE DATA!) --------------------------------------------------------------------------------- - --- Pozor: Toto zmaze vsetky data okrem services (tie sa importuju v CAST 3)! - -DELETE FROM todo_users; -DELETE FROM todos; -DELETE FROM company_remind; -DELETE FROM company_users; -DELETE FROM company_documents; -DELETE FROM companies; -DELETE FROM project_users; -DELETE FROM project_documents; -DELETE FROM projects; -DELETE FROM notes; -DELETE FROM time_entries; -DELETE FROM timesheets; -DELETE FROM event_users; -DELETE FROM events; -DELETE FROM messages; -DELETE FROM group_messages; -DELETE FROM chat_group_members; -DELETE FROM chat_groups; -DELETE FROM service_documents; -DELETE FROM service_folders; -DELETE FROM services; -DELETE FROM push_subscriptions; -DELETE FROM email_signatures; - -SELECT 'Cleanup hotovo!' as status; - -================================================================================ -KONIEC -================================================================================ diff --git a/ai-kurzy-tables.md b/ai-kurzy-tables.md deleted file mode 100644 index ea5333d..0000000 --- a/ai-kurzy-tables.md +++ /dev/null @@ -1,328 +0,0 @@ -# AI Kurzy - Drizzle Schema - -**Databázová schéma pre Node.js backend s Drizzle ORM** - ---- - -## 🗂️ Štruktúra Databázy - -**4 tabuľky:** - -``` -kurzy ←──┐ - │ - ├─→ registracie ←─→ ucastnici - │ │ - └───────────┴─→ prilohy -``` - -- **`kurzy`** - definície kurzov a termínov -- **`ucastnici`** - osobné údaje účastníkov -- **`registracie`** - väzba many-to-many (kto-kde-kedy + fakturácia) -- **`prilohy`** - dokumenty (certifikáty, faktúry) pripnuté k registráciám - ---- - -## 📄 Schéma: `src/db/schema.ts` - -```typescript -import { relations } from 'drizzle-orm'; -import { - pgTable, - serial, - varchar, - text, - numeric, - date, - integer, - boolean, - timestamp, - pgEnum, - bigint, - uniqueIndex, -} from 'drizzle-orm/pg-core'; - -// ============================================================================ -// ENUM DEFINÍCIE -// ============================================================================ - -export const formaKurzuEnum = pgEnum('forma_kurzu_enum', [ - 'prezenčne', - 'online', - 'hybridne', -]); - -export const stavRegistracieEnum = pgEnum('stav_registracie_enum', [ - 'potencialny', // Potenciálny záujemca - 'registrovany', // Registrovaný - 'potvrdeny', // Potvrdená účasť - 'absolvoval', // Absolvoval kurz (odškolené) - 'zruseny', // Zrušená registrácia -]); - -export const typPrilohyEnum = pgEnum('typ_prilohy_enum', [ - 'certifikat', - 'faktura', - 'prihlaska', - 'doklad_o_platbe', - 'ine', -]); - -// ============================================================================ -// TABUĽKA: kurzy -// ============================================================================ - -export const kurzy = pgTable('kurzy', { - id: serial('id').primaryKey(), - - // Základné informácie - nazov: varchar('nazov', { length: 255 }).notNull(), - typKurzu: varchar('typ_kurzu', { length: 100 }).notNull(), // "AI 1+2", "AI 1", "SEO" - popis: text('popis'), - - // Cenník - cena: numeric('cena', { precision: 10, scale: 2 }).notNull(), - - // Termín kurzu - datumOd: date('datum_od', { mode: 'date' }).notNull(), - datumDo: date('datum_do', { mode: 'date' }).notNull(), - - // Kapacita - maxKapacita: integer('max_kapacita'), // NULL = neobmedzené - - // Stav kurzu - aktivny: boolean('aktivny').default(true).notNull(), - - // Metadata - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); - -// ============================================================================ -// TABUĽKA: ucastnici -// ============================================================================ - -export const ucastnici = pgTable( - 'ucastnici', - { - id: serial('id').primaryKey(), - - // Osobné údaje - titul: varchar('titul', { length: 50 }), - meno: varchar('meno', { length: 100 }).notNull(), - priezvisko: varchar('priezvisko', { length: 100 }).notNull(), - - // Kontaktné údaje - email: varchar('email', { length: 255 }).notNull().unique(), - telefon: varchar('telefon', { length: 50 }), - - // Firemné údaje - firma: varchar('firma', { length: 255 }), - - // Adresa - mesto: varchar('mesto', { length: 100 }), - ulica: varchar('ulica', { length: 255 }), - psc: varchar('psc', { length: 10 }), - - // Metadata - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - emailIdx: uniqueIndex('ucastnici_email_idx').on(table.email), - }) -); - -// ============================================================================ -// TABUĽKA: registracie -// ============================================================================ - -export const registracie = pgTable( - 'registracie', - { - id: serial('id').primaryKey(), - - // Foreign keys - kurzId: integer('kurz_id') - .notNull() - .references(() => kurzy.id, { onDelete: 'cascade' }), - ucastnikId: integer('ucastnik_id') - .notNull() - .references(() => ucastnici.id, { onDelete: 'cascade' }), - - // Forma účasti - formaKurzu: formaKurzuEnum('forma_kurzu').default('prezenčne').notNull(), - - // Počet účastníkov (ak firma prihlasuje viacerých) - pocetUcastnikov: integer('pocet_ucastnikov').default(1).notNull(), - - // Fakturácia - fakturaCislo: varchar('faktura_cislo', { length: 100 }), - fakturaVystavena: boolean('faktura_vystavena').default(false).notNull(), - zaplatene: boolean('zaplatene').default(false).notNull(), - - // Stav registrácie - stav: stavRegistracieEnum('stav').default('registrovany').notNull(), - - // Poznámky - poznamka: text('poznamka'), - - // Metadata - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - // Unikátne obmedzenie: jeden účastník sa nemôže prihlásiť na ten istý kurz viackrát - uniqRegistracia: uniqueIndex('registracie_kurz_ucastnik_idx').on( - table.kurzId, - table.ucastnikId - ), - }) -); - -// ============================================================================ -// TABUĽKA: prilohy -// ============================================================================ - -export const prilohy = pgTable('prilohy', { - id: serial('id').primaryKey(), - - // Foreign key - registraciaId: integer('registracia_id') - .notNull() - .references(() => registracie.id, { onDelete: 'cascade' }), - - // Informácie o súbore - nazovSuboru: varchar('nazov_suboru', { length: 255 }).notNull(), - typPrilohy: typPrilohyEnum('typ_prilohy').default('ine').notNull(), - - // Úložisko - cestaKSuboru: varchar('cesta_k_suboru', { length: 500 }).notNull(), - mimeType: varchar('mime_type', { length: 100 }), - velkostSuboru: bigint('velkost_suboru', { mode: 'number' }), // veľkosť v bytoch - - // Popis - popis: text('popis'), - - // Metadata - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); - -// ============================================================================ -// RELAČNÉ VÄZBY (Relations) -// ============================================================================ - -// Kurzy Relations -export const kurzyRelations = relations(kurzy, ({ many }) => ({ - registracie: many(registracie), -})); - -// Účastníci Relations -export const ucastniciRelations = relations(ucastnici, ({ many }) => ({ - registracie: many(registracie), -})); - -// Registrácie Relations -export const registracieRelations = relations(registracie, ({ one, many }) => ({ - kurz: one(kurzy, { - fields: [registracie.kurzId], - references: [kurzy.id], - }), - ucastnik: one(ucastnici, { - fields: [registracie.ucastnikId], - references: [ucastnici.id], - }), - prilohy: many(prilohy), -})); - -// Prílohy Relations -export const prilohyRelations = relations(prilohy, ({ one }) => ({ - registracia: one(registracie, { - fields: [prilohy.registraciaId], - references: [registracie.id], - }), -})); -``` - ---- - -## 📘 TypeScript Typy - -```typescript -import { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { kurzy, ucastnici, registracie, prilohy } from './schema'; - -// SELECT typy (čítanie z DB) -export type Kurz = InferSelectModel; -export type Ucastnik = InferSelectModel; -export type Registracia = InferSelectModel; -export type Priloha = InferSelectModel; - -// INSERT typy (vkladanie do DB) -export type NewKurz = InferInsertModel; -export type NewUcastnik = InferInsertModel; -export type NewRegistracia = InferInsertModel; -export type NewPriloha = InferInsertModel; -``` - ---- - -## 🔗 Vysvetlenie Relácií - -### **1:N (One-to-Many)** -```typescript -// Jeden kurz má VIAC registrácií -kurzyRelations = relations(kurzy, ({ many }) => ({ - registracie: many(registracie), -})); -``` - -### **N:1 (Many-to-One)** -```typescript -// Viac registrácií patrí k JEDNÉMU kurzu -registracieRelations = relations(registracie, ({ one }) => ({ - kurz: one(kurzy, { - fields: [registracie.kurzId], // FK - references: [kurzy.id], // PK - }), -})); -``` - -### **N:M (Many-to-Many)** -``` -kurzy ↔ registracie ↔ ucastnici -- Jeden kurz má viac účastníkov -- Jeden účastník môže ísť na viac kurzov -- Junction table: registracie -``` - ---- - -## 💡 Použitie - -### Základný query s relačnými dátami - -```typescript -import { db } from './db'; -import { kurzy } from './db/schema'; -import { eq } from 'drizzle-orm'; - -// Kurz s registráciami a účastníkmi -const kurzDetail = await db.query.kurzy.findFirst({ - where: eq(kurzy.id, 1), - with: { - registracie: { - with: { - ucastnik: true, - prilohy: true, - }, - }, - }, -}); -``` - ---- - -**Vytvorené:** 2026-01-20 -**Stack:** Node.js + Drizzle ORM + PostgreSQL + TypeScript diff --git a/package.json b/package.json index 3608889..ff4f2b9 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,7 @@ "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "db:seed": "node src/db/seeds/admin.seed.js", - "db:seed:testuser": "node src/db/seeds/testuser.seed.js", - "db:import:ai-kurzy": "node src/db/seeds/ai-kurzy-import.seed.js", - "db:import:ai-kurzy-csv": "node src/db/seeds/ai-kurzy-csv-import.seed.js" + "db:seed:testuser": "node src/db/seeds/testuser.seed.js" }, "keywords": [], "author": "Richard Tekula", diff --git a/sql-fix.txt b/sql-fix.txt deleted file mode 100644 index 1800e1d..0000000 --- a/sql-fix.txt +++ /dev/null @@ -1,34 +0,0 @@ -SQL príkazy pre Coolify: - - -- 1. NAJPRV: Pozri koľko dát sa vymaže - SELECT - (SELECT COUNT(*) FROM email_accounts) as email_accounts, - (SELECT COUNT(*) FROM user_email_accounts) as user_email_accounts, - (SELECT COUNT(*) FROM contacts) as contacts, - (SELECT COUNT(*) FROM emails) as emails; - - -- 2. VYMAŽ všetko (cascade sa postará o zvyšok) - DELETE FROM email_accounts; - - -- 3. OVER že je všetko prázdne - SELECT - (SELECT COUNT(*) FROM email_accounts) as email_accounts, - (SELECT COUNT(*) FROM user_email_accounts) as user_email_accounts, - (SELECT COUNT(*) FROM contacts) as contacts, - (SELECT COUNT(*) FROM emails) as emails; - - -- 4. SPUSTI INDEXY - CREATE INDEX IF NOT EXISTS idx_contacts_email_account_id ON contacts(email_account_id); - CREATE INDEX IF NOT EXISTS idx_contacts_company_id ON contacts(company_id); - CREATE INDEX IF NOT EXISTS idx_todos_project_id ON todos(project_id); - CREATE INDEX IF NOT EXISTS idx_todos_company_id ON todos(company_id); - CREATE INDEX IF NOT EXISTS idx_notes_company_id ON notes(company_id); - CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id); - CREATE INDEX IF NOT EXISTS idx_notes_todo_id ON notes(todo_id); - CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email); - CREATE INDEX IF NOT EXISTS idx_companies_name ON companies(name); - CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name); - CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status); - CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status); - CREATE INDEX IF NOT EXISTS idx_todos_user_status ON todo_users(user_id, todo_id); - CREATE INDEX IF NOT EXISTS idx_time_entries_user_start ON time_entries(user_id, start_time); \ No newline at end of file diff --git a/sql/01_schema_migration.sql b/sql/01_schema_migration.sql deleted file mode 100644 index 777b197..0000000 --- a/sql/01_schema_migration.sql +++ /dev/null @@ -1,230 +0,0 @@ --- ============================================================ --- COMPLETE SCHEMA MIGRATION FOR COOLIFY --- Run this first to update the database schema --- ============================================================ - --- Create ENUMs for AI Kurzy (if not exist) -DO $$ BEGIN - CREATE TYPE "forma_kurzu_enum" AS ENUM('prezencne', 'online', 'hybridne'); -EXCEPTION WHEN duplicate_object THEN NULL; -END $$; - -DO $$ BEGIN - CREATE TYPE "stav_registracie_enum" AS ENUM('potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny'); -EXCEPTION WHEN duplicate_object THEN NULL; -END $$; - -DO $$ BEGIN - CREATE TYPE "typ_prilohy_enum" AS ENUM('certifikat', 'faktura', 'prihlaska', 'doklad_o_platbe', 'ine'); -EXCEPTION WHEN duplicate_object THEN NULL; -END $$; - --- ============================================================ --- NEW TABLES --- ============================================================ - --- Chat Groups -CREATE TABLE IF NOT EXISTS "chat_groups" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "name" text NOT NULL, - "created_by_id" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); - --- Chat Group Members -CREATE TABLE IF NOT EXISTS "chat_group_members" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "group_id" uuid NOT NULL REFERENCES "chat_groups"("id") ON DELETE CASCADE, - "user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, - "joined_at" timestamp DEFAULT now() NOT NULL, - "last_read_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "chat_group_member_unique" UNIQUE("group_id","user_id") -); - --- Group Messages -CREATE TABLE IF NOT EXISTS "group_messages" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "group_id" uuid NOT NULL REFERENCES "chat_groups"("id") ON DELETE CASCADE, - "sender_id" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "content" text NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); - --- Push Subscriptions -CREATE TABLE IF NOT EXISTS "push_subscriptions" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, - "endpoint" text NOT NULL, - "p256dh" text NOT NULL, - "auth" text NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "push_subscription_endpoint_unique" UNIQUE("user_id","endpoint") -); - --- Email Signatures -CREATE TABLE IF NOT EXISTS "email_signatures" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL UNIQUE REFERENCES "users"("id") ON DELETE CASCADE, - "full_name" text, - "position" text, - "phone" text, - "email" text, - "company_name" text, - "website" text, - "is_enabled" boolean DEFAULT true NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); - --- Services -CREATE TABLE IF NOT EXISTS "services" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "name" text NOT NULL, - "price" text NOT NULL, - "description" text, - "created_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); - --- Service Folders -CREATE TABLE IF NOT EXISTS "service_folders" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "name" text NOT NULL, - "created_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); - --- Service Documents -CREATE TABLE IF NOT EXISTS "service_documents" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "folder_id" uuid NOT NULL REFERENCES "service_folders"("id") ON DELETE CASCADE, - "file_name" text NOT NULL, - "original_name" text NOT NULL, - "file_path" text NOT NULL, - "file_type" text NOT NULL, - "file_size" integer NOT NULL, - "description" text, - "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "uploaded_at" timestamp DEFAULT now() NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); - --- Company Documents -CREATE TABLE IF NOT EXISTS "company_documents" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "company_id" uuid NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE, - "file_name" text NOT NULL, - "original_name" text NOT NULL, - "file_path" text NOT NULL, - "file_type" text NOT NULL, - "file_size" integer NOT NULL, - "description" text, - "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "uploaded_at" timestamp DEFAULT now() NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); - --- Project Documents -CREATE TABLE IF NOT EXISTS "project_documents" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "project_id" uuid NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE, - "file_name" text NOT NULL, - "original_name" text NOT NULL, - "file_path" text NOT NULL, - "file_type" text NOT NULL, - "file_size" integer NOT NULL, - "description" text, - "uploaded_by" uuid REFERENCES "users"("id") ON DELETE SET NULL, - "uploaded_at" timestamp DEFAULT now() NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); - --- ============================================================ --- AI KURZY TABLES --- ============================================================ - --- Kurzy (Courses) - without dates (dates are per registration) -CREATE TABLE IF NOT EXISTS "kurzy" ( - "id" serial PRIMARY KEY NOT NULL, - "nazov" varchar(255) NOT NULL, - "typ_kurzu" varchar(100) NOT NULL, - "popis" text, - "cena" numeric(10, 2) NOT NULL, - "max_kapacita" integer, - "aktivny" boolean DEFAULT true NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); - --- Ucastnici (Participants) -CREATE TABLE IF NOT EXISTS "ucastnici" ( - "id" serial PRIMARY KEY NOT NULL, - "titul" varchar(50), - "meno" varchar(100) NOT NULL, - "priezvisko" varchar(100) NOT NULL, - "email" varchar(255) NOT NULL UNIQUE, - "telefon" varchar(50), - "firma" varchar(255), - "mesto" varchar(100), - "ulica" varchar(255), - "psc" varchar(10), - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); - -CREATE UNIQUE INDEX IF NOT EXISTS "ucastnici_email_idx" ON "ucastnici" USING btree ("email"); - --- Registracie (Registrations) - with dates -CREATE TABLE IF NOT EXISTS "registracie" ( - "id" serial PRIMARY KEY NOT NULL, - "kurz_id" integer NOT NULL REFERENCES "kurzy"("id") ON DELETE CASCADE, - "ucastnik_id" integer NOT NULL REFERENCES "ucastnici"("id") ON DELETE CASCADE, - "datum_od" date, - "datum_do" date, - "forma_kurzu" "forma_kurzu_enum" DEFAULT 'prezencne' NOT NULL, - "pocet_ucastnikov" integer DEFAULT 1 NOT NULL, - "faktura_cislo" varchar(100), - "faktura_vystavena" boolean DEFAULT false NOT NULL, - "zaplatene" boolean DEFAULT false NOT NULL, - "stav" "stav_registracie_enum" DEFAULT 'registrovany' NOT NULL, - "poznamka" text, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); - -CREATE UNIQUE INDEX IF NOT EXISTS "registracie_kurz_ucastnik_idx" ON "registracie" USING btree ("kurz_id","ucastnik_id"); - --- Prilohy (Attachments) -CREATE TABLE IF NOT EXISTS "prilohy" ( - "id" serial PRIMARY KEY NOT NULL, - "registracia_id" integer NOT NULL REFERENCES "registracie"("id") ON DELETE CASCADE, - "nazov_suboru" varchar(255) NOT NULL, - "typ_prilohy" "typ_prilohy_enum" DEFAULT 'ine' NOT NULL, - "cesta_k_suboru" varchar(500) NOT NULL, - "mime_type" varchar(100), - "velkost_suboru" bigint, - "popis" text, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); - --- ============================================================ --- ALTER EXISTING TABLES (add new columns) --- ============================================================ - --- Add completed_notified_at to todos (if not exists) -DO $$ BEGIN - ALTER TABLE "todos" ADD COLUMN "completed_notified_at" timestamp; -EXCEPTION WHEN duplicate_column THEN NULL; -END $$; - --- Make phone nullable in personal_contacts (if needed) -ALTER TABLE "personal_contacts" ALTER COLUMN "phone" DROP NOT NULL; - --- ============================================================ --- DONE --- ============================================================ -SELECT 'Schema migration completed successfully!' as status; diff --git a/sql/02_ai_kurzy_company_details.sql b/sql/02_ai_kurzy_company_details.sql deleted file mode 100644 index 58176b4..0000000 --- a/sql/02_ai_kurzy_company_details.sql +++ /dev/null @@ -1,19 +0,0 @@ --- ============================================================ --- AI KURZY - Company Details & Course Colors Migration --- Run this FIRST to add new columns --- ============================================================ - --- Add color field to kurzy table -ALTER TABLE "kurzy" ADD COLUMN IF NOT EXISTS "farba" varchar(20); - --- Add company details fields to ucastnici table -ALTER TABLE "ucastnici" ADD COLUMN IF NOT EXISTS "firma_ico" varchar(20); -ALTER TABLE "ucastnici" ADD COLUMN IF NOT EXISTS "firma_dic" varchar(20); -ALTER TABLE "ucastnici" ADD COLUMN IF NOT EXISTS "firma_ic_dph" varchar(25); -ALTER TABLE "ucastnici" ADD COLUMN IF NOT EXISTS "firma_sidlo" text; - --- ============================================================ --- DONE - Now run 03_ai_kurzy_full_data.sql for complete data --- ============================================================ -SELECT 'AI Kurzy schema migration completed!' as status; -SELECT 'Now run 03_ai_kurzy_full_data.sql for complete data' as next_step; diff --git a/sql/02_ai_kurzy_data.sql b/sql/02_ai_kurzy_data.sql deleted file mode 100644 index d36f9ea..0000000 --- a/sql/02_ai_kurzy_data.sql +++ /dev/null @@ -1,97 +0,0 @@ --- ============================================================ --- AI KURZY DATA IMPORT --- Run this after schema migration to import course data --- ============================================================ - --- Clear existing AI Kurzy data (optional - remove if you want to keep existing data) -DELETE FROM prilohy; -DELETE FROM registracie; -DELETE FROM ucastnici; -DELETE FROM kurzy; - --- Reset sequences -ALTER SEQUENCE kurzy_id_seq RESTART WITH 1; -ALTER SEQUENCE ucastnici_id_seq RESTART WITH 1; -ALTER SEQUENCE registracie_id_seq RESTART WITH 1; -ALTER SEQUENCE prilohy_id_seq RESTART WITH 1; - --- ============================================================ --- INSERT COURSES (without dates - dates are per registration) --- ============================================================ - -INSERT INTO kurzy (nazov, typ_kurzu, cena, aktivny) VALUES -('AI 1+2 (2 dni) - 290€', 'AI', 290.00, true), -('AI 1 (1 deň) - 150€', 'AI', 150.00, true), -('AI 2 (1 deň) - 150€', 'AI', 150.00, true), -('AI v SEO (1 deň) - 150€', 'SEO', 150.00, true), -('AI I+II Marec 2026', 'AI', 290.00, true), -('AI I+II Apríl 2026', 'AI', 290.00, true); - --- ============================================================ --- INSERT PARTICIPANTS --- ============================================================ - -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, mesto, ulica, psc) VALUES -(NULL, 'Martin', 'Sovák', 'info@energium.sk', '0918986172', 'energium sro', 'Bratislava', 'Topolcianska 5', '85105'), -(NULL, 'Michal', 'Farkaš', 'michal.farkas83@gmail.com', '0911209122', 'SLOVWELD', 'Dunajska Lužná', 'Mandlova 30', '90042'), -(NULL, 'Alena', 'Šranková', 'alena.srankova@gmail.com', '0917352580', NULL, 'Bratislava', 'Šándorova 1', '82103'), -(NULL, 'Katarina', 'Tomaníková', 'k.tomanikova@riseday.net', '0948 070 611', 'Classica Shipping Limited', 'Bratislava', 'Keltska 104', '85110'), -(NULL, 'Róbert', 'Brišák', 'robert.brisak@ss-nizna.sk', '0910583883', 'Spojená škola, Hattalova 471, 02743 Nižná', 'Nižná', 'Hattalova 471', '02743'), -(NULL, 'Marián', 'Bača', 'baca.marian@gmail.com', '0907994126', NULL, 'Petrovany', '8', '08253'), -('Mgr. MBA', 'Nikola', 'Horáčková', 'nikolahorackova11@gmail.com', '0918482184', NULL, 'Zákopčie', 'Zákopčie stred 12', '023 11'), -(NULL, 'Tomáš', 'Kupec', 'kupec.tom@gmail.com', '0911030190', 'Jamajka', 'Liptovská Sielnica', NULL, '032 23'), -(NULL, 'Anton', 'Považský', 'anton.povazsky@example.com', NULL, NULL, NULL, NULL, NULL); - --- ============================================================ --- INSERT REGISTRATIONS (with dates) --- ============================================================ - --- AI 1+2 (2 dni) - Februar 2026 -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026020' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'info@energium.sk'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'online', 1, true, true, 'registrovany', NULL -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'alena.srankova@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, true, 'registrovany', 'presunuta z oktobra, chce až január' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'k.tomanikova@riseday.net'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026019' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'robert.brisak@ss-nizna.sk'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, false, false, 'potencialny', 'vzdelávací poukaz' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni) - 290€' AND u.email = 'nikolahorackova11@gmail.com'; - --- AI 1 (1 den) - Februar 2026 -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-02', 'online', 1, true, true, 'registrovany', 'Fa 2025 338, Súhlasil so zmeneným termínom' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1 (1 deň) - 150€' AND u.email = 'michal.farkas83@gmail.com'; - --- AI 2 (1 den) - Februar 2026 -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-03', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'Fa Gablasova' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 2 (1 deň) - 150€' AND u.email = 'baca.marian@gmail.com'; - --- AI v SEO - Februar 2026 -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', 'FA 2026021' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO (1 deň) - 150€' AND u.email = 'kupec.tom@gmail.com'; - -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', NULL -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO (1 deň) - 150€' AND u.email = 'anton.povazsky@example.com'; - --- ============================================================ --- VERIFY DATA --- ============================================================ - -SELECT 'Courses imported:' as info, COUNT(*) as count FROM kurzy; -SELECT 'Participants imported:' as info, COUNT(*) as count FROM ucastnici; -SELECT 'Registrations imported:' as info, COUNT(*) as count FROM registracie; - -SELECT 'AI Kurzy data import completed successfully!' as status; diff --git a/sql/03_ai_kurzy_full_data.sql b/sql/03_ai_kurzy_full_data.sql deleted file mode 100644 index 7567aba..0000000 --- a/sql/03_ai_kurzy_full_data.sql +++ /dev/null @@ -1,135 +0,0 @@ --- ============================================================ --- AI KURZY - Complete Data Reset and Import --- Run this to start fresh with proper data --- ============================================================ - --- First, clear existing data -DELETE FROM prilohy; -DELETE FROM registracie; -DELETE FROM ucastnici; -DELETE FROM kurzy; - --- Reset sequences -ALTER SEQUENCE kurzy_id_seq RESTART WITH 1; -ALTER SEQUENCE ucastnici_id_seq RESTART WITH 1; -ALTER SEQUENCE registracie_id_seq RESTART WITH 1; - --- ============================================================ --- INSERT COURSES (without dates in names, with colors) --- ============================================================ -INSERT INTO kurzy (nazov, typ_kurzu, cena, aktivny, farba) VALUES - ('AI 1 (1 deň)', 'AI', 150.00, true, 'blue'), - ('AI 2 (1 deň)', 'AI', 150.00, true, 'emerald'), - ('AI 1+2 (2 dni)', 'AI', 290.00, true, 'violet'), - ('AI v SEO', 'SEO', 150.00, true, 'amber'); - --- ============================================================ --- INSERT PARTICIPANTS with company details --- ============================================================ - --- 1. Martin Sovák - energium sro -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Martin', 'Sovák', 'info@energium.sk', '0918986172', 'energium sro', '47613033', '2024004433', 'SK2024004433', 'Topolcianska 5, 85105 Bratislava', 'Bratislava', 'Topolcianska 5', '85105'); - --- 2. Michal Farkaš - SLOVWELD -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Michal', 'Farkaš', 'michal.farkas83@gmail.com', '0911209122', 'SLOVWELD', NULL, NULL, NULL, NULL, 'Dunajska Lužná', 'Mandlova 30', '90042'); - --- 3. Alena Šranková - bez firmy -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Alena', 'Šranková', 'alena.srankova@gmail.com', '0917352580', NULL, NULL, NULL, NULL, NULL, 'Bratislava', 'Šándorova 1', '82103'); - --- 4. Katarina Tomaníková - Classica Shipping Limited -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Katarina', 'Tomaníková', 'k.tomanikova@riseday.net', '0948070611', 'Classica Shipping Limited', NULL, NULL, NULL, NULL, 'Bratislava', 'Keltska 104', '85110'); - --- 5. Róbert Brišák - Spojená škola -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Róbert', 'Brišák', 'robert.brisak@ss-nizna.sk', '0910583883', 'Spojená škola Nižná', NULL, NULL, NULL, 'Hattalova 471, 02743 Nižná', 'Nižná', 'Hattalova 471', '02743'); - --- 6. Marián Bača - bez firmy -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Marián', 'Bača', 'baca.marian@gmail.com', '0907994126', NULL, NULL, NULL, NULL, NULL, 'Petrovany', '8', '08253'); - --- 7. Nikola Horáčková - bez firmy -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES ('Mgr. MBA', 'Nikola', 'Horáčková', 'nikolahorackova11@gmail.com', '0918482184', NULL, NULL, NULL, NULL, NULL, 'Zákopčie', 'Zákopčie stred 12', '02311'); - --- 8. Tomáš Kupec - Jamajka s.r.o. -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Tomáš', 'Kupec', 'kupec.tom@gmail.com', '0911030190', 'JAMAJKA, s.r.o.', '36411833', '2020128539', 'SK2020128539', 'Hotel Koliba Gréta 270 -032 23 Liptovská Sielnica', 'Liptovská Sielnica', NULL, '03223'); - --- 9. Anton Považský - bez firmy (testovací) -INSERT INTO ucastnici (titul, meno, priezvisko, email, telefon, firma, firma_ico, firma_dic, firma_ic_dph, firma_sidlo, mesto, ulica, psc) -VALUES (NULL, 'Anton', 'Považský', 'anton.povazsky@example.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); - --- ============================================================ --- INSERT REGISTRATIONS --- ============================================================ - --- Martin Sovák -> AI 1+2 (2 dni) - prezenčne, faktúra vystavená, nezaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026020' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni)' AND u.email = 'info@energium.sk'; - --- Michal Farkaš -> AI 1 (1 deň) - online, zaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-02', 'online', 1, true, true, 'registrovany', 'Fa 2025 338, Súhlasil so zmeneným termínom' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1 (1 deň)' AND u.email = 'michal.farkas83@gmail.com'; - --- Alena Šranková -> AI 1+2 (2 dni) - online, zaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'online', 1, true, true, 'registrovany', NULL -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni)' AND u.email = 'alena.srankova@gmail.com'; - --- Katarina Tomaníková -> AI 1+2 (2 dni) - prezenčne, zaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, true, 'registrovany', 'Presunuta z októbra, chcela až január' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni)' AND u.email = 'k.tomanikova@riseday.net'; - --- Róbert Brišák -> AI 1+2 (2 dni) - prezenčne, nezaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'FA 2026019' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni)' AND u.email = 'robert.brisak@ss-nizna.sk'; - --- Marián Bača -> AI 2 (1 deň) - prezenčne, nezaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-03', '2026-02-03', 'prezencne', 1, true, false, 'registrovany', 'Fa Gablasova' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 2 (1 deň)' AND u.email = 'baca.marian@gmail.com'; - --- Nikola Horáčková -> AI 1+2 (2 dni) - potenciálny (vzdelávací poukaz) -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-02', '2026-02-03', 'prezencne', 1, false, false, 'potencialny', 'Vzdelávací poukaz' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI 1+2 (2 dni)' AND u.email = 'nikolahorackova11@gmail.com'; - --- Tomáš Kupec -> AI v SEO - prezenčne, nezaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', 'FA 2026021' -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO' AND u.email = 'kupec.tom@gmail.com'; - --- Anton Považský -> AI v SEO - prezenčne, nezaplatené -INSERT INTO registracie (kurz_id, ucastnik_id, datum_od, datum_do, forma_kurzu, pocet_ucastnikov, faktura_vystavena, zaplatene, stav, poznamka) -SELECT k.id, u.id, '2026-02-13', '2026-02-13', 'prezencne', 1, true, false, 'registrovany', NULL -FROM kurzy k, ucastnici u WHERE k.nazov = 'AI v SEO' AND u.email = 'anton.povazsky@example.com'; - --- ============================================================ --- VERIFICATION --- ============================================================ -SELECT 'Kurzy:' as info, COUNT(*) as pocet FROM kurzy; -SELECT 'Účastníci:' as info, COUNT(*) as pocet FROM ucastnici; -SELECT 'Registrácie:' as info, COUNT(*) as pocet FROM registracie; - --- Show sample data with company details -SELECT - u.meno || ' ' || u.priezvisko as ucastnik, - u.firma, - u.firma_ico as ico, - u.firma_dic as dic, - k.nazov as kurz, - k.farba, - r.zaplatene -FROM registracie r -JOIN ucastnici u ON r.ucastnik_id = u.id -JOIN kurzy k ON r.kurz_id = k.id -ORDER BY r.datum_od; diff --git a/sql/03_cleanup_data.sql b/sql/03_cleanup_data.sql deleted file mode 100644 index 45f5bfc..0000000 --- a/sql/03_cleanup_data.sql +++ /dev/null @@ -1,82 +0,0 @@ --- ============================================================ --- CLEANUP DATA - Clear test/development data --- WARNING: This will DELETE data! Use with caution! --- ============================================================ - --- ============================================================ --- OPTION 1: SOFT CLEANUP (keeps structure, removes data) --- ============================================================ - --- Clear Todos and related -DELETE FROM todo_users; -DELETE FROM todos; - --- Clear Companies and related -DELETE FROM company_remind; -DELETE FROM company_users; -DELETE FROM company_documents; -DELETE FROM companies; - --- Clear Projects and related -DELETE FROM project_users; -DELETE FROM project_documents; -DELETE FROM projects; - --- Clear Notes -DELETE FROM notes; - --- Clear Time Entries -DELETE FROM time_entries; - --- Clear Timesheets -DELETE FROM timesheets; - --- Clear Events and related -DELETE FROM event_users; -DELETE FROM events; - --- Clear Messages (both direct and group) -DELETE FROM messages; -DELETE FROM group_messages; -DELETE FROM chat_group_members; -DELETE FROM chat_groups; - --- Clear Services -DELETE FROM service_documents; -DELETE FROM service_folders; -DELETE FROM services; - --- Clear Email related (contacts, emails) - BE CAREFUL --- DELETE FROM emails; --- DELETE FROM contacts; - --- Clear Push subscriptions -DELETE FROM push_subscriptions; - --- Clear Email signatures -DELETE FROM email_signatures; - --- ============================================================ --- OPTION 2: RESET SEQUENCES (optional) --- ============================================================ - --- If you have serial IDs and want to reset them: --- ALTER SEQUENCE todos_id_seq RESTART WITH 1; --- etc. - --- ============================================================ --- VERIFY CLEANUP --- ============================================================ - -SELECT 'Cleanup results:' as info; -SELECT 'Todos:' as table_name, COUNT(*) as remaining FROM todos; -SELECT 'Companies:' as table_name, COUNT(*) as remaining FROM companies; -SELECT 'Projects:' as table_name, COUNT(*) as remaining FROM projects; -SELECT 'Notes:' as table_name, COUNT(*) as remaining FROM notes; -SELECT 'Events:' as table_name, COUNT(*) as remaining FROM events; -SELECT 'Messages:' as table_name, COUNT(*) as remaining FROM messages; -SELECT 'Chat Groups:' as table_name, COUNT(*) as remaining FROM chat_groups; -SELECT 'Time Entries:' as table_name, COUNT(*) as remaining FROM time_entries; -SELECT 'Services:' as table_name, COUNT(*) as remaining FROM services; - -SELECT 'Cleanup completed!' as status; diff --git a/sql/README.md b/sql/README.md deleted file mode 100644 index e065fec..0000000 --- a/sql/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# SQL Migration Scripts - -## Pouzitie na Coolify - -### 1. Pripojenie k databaze cez terminal - -```bash -# Na Coolify serveri najdi PostgreSQL container a pripoj sa -docker exec -it psql -U -d - -# Alebo ak mas pristup cez SSH: -psql -h localhost -U -d -``` - -### 2. Poradie spustenia scriptov - -**DOLEZITE: Spusti scripty v tomto poradi!** - -```sql --- 1. Najprv schema migration (vytvori nove tabulky a stlpce) -\i /path/to/01_schema_migration.sql - --- 2. Potom AI Kurzy data (vlozi kurzy a ucastnikov) -\i /path/to/02_ai_kurzy_data.sql - --- 3. Volitelne: Cleanup existujucich dat (POZOR - maze data!) -\i /path/to/03_cleanup_data.sql -``` - -### 3. Alternativne - copy/paste - -Mozes tiez otvorit subory a copy/paste obsah priamo do psql terminalu. - -### Popis suborov - -| Subor | Popis | -|-------|-------| -| `01_schema_migration.sql` | Vytvori vsetky nove tabulky (chat groups, push notifications, AI kurzy, atd.) a prida nove stlpce do existujucich tabuliek | -| `02_ai_kurzy_data.sql` | Importuje kurzy a ucastnikov z CSV - 6 kurzov, 9 ucastnikov | -| `03_cleanup_data.sql` | Vymaze test data z todos, companies, projects, notes, events, messages | - -### Poznamky - -- `01_schema_migration.sql` je bezpecny - pouziva `IF NOT EXISTS` takze nevytvori duplicity -- `02_ai_kurzy_data.sql` najprv ZMAZE existujuce AI kurzy data! -- `03_cleanup_data.sql` ZMAZE data! Pouzi opatrne! diff --git a/src/db/seeds/ai-kurzy-csv-import.seed.js b/src/db/seeds/ai-kurzy-csv-import.seed.js deleted file mode 100644 index 235fb6b..0000000 --- a/src/db/seeds/ai-kurzy-csv-import.seed.js +++ /dev/null @@ -1,303 +0,0 @@ -import dotenv from 'dotenv'; -dotenv.config(); - -import { eq, sql } from 'drizzle-orm'; - -const { db } = await import('../../config/database.js'); -const { kurzy, ucastnici, registracie } = await import('../schema.js'); - -// Clear existing data -async function clearData() { - console.log('Clearing existing data...'); - await db.delete(registracie); - await db.delete(ucastnici); - await db.delete(kurzy); - // Reset sequences - await db.execute(sql`ALTER SEQUENCE kurzy_id_seq RESTART WITH 1`); - await db.execute(sql`ALTER SEQUENCE ucastnici_id_seq RESTART WITH 1`); - await db.execute(sql`ALTER SEQUENCE registracie_id_seq RESTART WITH 1`); - console.log('Data cleared.'); -} - -// Course data - now without dates (dates are per-registration) -const coursesData = [ - { - nazov: 'AI 1+2 (2 dni) - 290€', - typKurzu: 'AI', - cena: '290', - }, - { - nazov: 'AI 1 (1 deň) - 150€', - typKurzu: 'AI', - cena: '150', - }, - { - nazov: 'AI 2 (1 deň) - 150€', - typKurzu: 'AI', - cena: '150', - }, - { - nazov: 'AI v SEO (1 deň) - 150€', - typKurzu: 'SEO', - cena: '150', - }, - { - nazov: 'AI I+II Marec 2026', - typKurzu: 'AI', - cena: '290', - }, - { - nazov: 'AI I+II Apríl 2026', - typKurzu: 'AI', - cena: '290', - }, -]; - -// Participants data from CSV - dates are now on registration level -const participantsData = [ - // Umelá Inteligencia I+II 2. - 3. Február 2026 - { - meno: 'Martin', - priezvisko: 'Sovák', - telefon: '0918986172', - email: 'info@energium.sk', - firma: 'energium sro', - formaKurzu: 'prezencne', - kurz: 'AI 1+2 (2 dni) - 290€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Bratislava', - ulica: 'Topolcianska 5', - psc: '85105', - fakturaVystavena: true, - zaplatene: false, - poznamka: 'FA 2026020', - stav: 'registrovany', - }, - { - meno: 'Michal', - priezvisko: 'Farkaš', - telefon: '0911209122', - email: 'michal.farkas83@gmail.com', - firma: 'SLOVWELD', - formaKurzu: 'online', - kurz: 'AI 1 (1 deň) - 150€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-02'), - pocetUcastnikov: 1, - mesto: 'Dunajska Lužná', - ulica: 'Mandlova 30', - psc: '90042', - fakturaVystavena: true, - zaplatene: true, - poznamka: 'Fa 2025 338, Súhlasil so zmeneným termínom', - stav: 'registrovany', - }, - { - meno: 'Alena', - priezvisko: 'Šranková', - telefon: '0917352580', - email: 'alena.srankova@gmail.com', - formaKurzu: 'online', - kurz: 'AI 1+2 (2 dni) - 290€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Bratislava', - ulica: 'Šándorova 1', - psc: '82103', - fakturaVystavena: true, - zaplatene: true, - stav: 'registrovany', - }, - { - meno: 'Katarina', - priezvisko: 'Tomaníková', - telefon: '0948 070 611', - email: 'k.tomanikova@riseday.net', - firma: 'Classica Shipping Limited', - formaKurzu: 'prezencne', - kurz: 'AI 1+2 (2 dni) - 290€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Bratislava', - ulica: 'Keltska 104', - psc: '85110', - fakturaVystavena: true, - zaplatene: true, - poznamka: 'presunuta z oktobra, chce až január', - stav: 'registrovany', - }, - { - meno: 'Róbert', - priezvisko: 'Brišák', - telefon: '0910583883', - email: 'robert.brisak@ss-nizna.sk', - firma: 'Spojená škola, Hattalova 471, 02743 Nižná', - formaKurzu: 'prezencne', - kurz: 'AI 1+2 (2 dni) - 290€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Nižná', - ulica: 'Hattalova 471', - psc: '02743', - fakturaVystavena: true, - zaplatene: false, - poznamka: 'FA 2026019', - stav: 'registrovany', - }, - { - meno: 'Marián', - priezvisko: 'Bača', - telefon: '0907994126', - email: 'baca.marian@gmail.com', - formaKurzu: 'prezencne', - kurz: 'AI 2 (1 deň) - 150€', - datumOd: new Date('2026-02-03'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Petrovany', - ulica: '8', - psc: '08253', - fakturaVystavena: true, - zaplatene: false, - poznamka: 'Fa Gablasova', - stav: 'registrovany', - }, - { - titul: 'Mgr. MBA', - meno: 'Nikola', - priezvisko: 'Horáčková', - telefon: '0918482184', - email: 'nikolahorackova11@gmail.com', - kurz: 'AI 1+2 (2 dni) - 290€', - datumOd: new Date('2026-02-02'), - datumDo: new Date('2026-02-03'), - pocetUcastnikov: 1, - mesto: 'Zákopčie', - ulica: 'Zákopčie stred 12', - psc: '023 11', - fakturaVystavena: false, - zaplatene: false, - poznamka: 'vzdelávací poukaz', - stav: 'potencialny', - }, - // AI v SEO 13.2.2026 - { - meno: 'Tomáš', - priezvisko: 'Kupec', - telefon: '0911030190', - email: 'kupec.tom@gmail.com', - firma: 'Jamajka', - formaKurzu: 'prezencne', - kurz: 'AI v SEO (1 deň) - 150€', - datumOd: new Date('2026-02-13'), - datumDo: new Date('2026-02-13'), - pocetUcastnikov: 1, - mesto: 'Liptovská Sielnica', - psc: '032 23', - fakturaVystavena: true, - zaplatene: false, - poznamka: 'FA 2026021', - stav: 'registrovany', - }, - { - meno: 'Anton', - priezvisko: 'Považský', - email: 'anton.povazsky@example.com', // No email in CSV, using placeholder - formaKurzu: 'prezencne', - kurz: 'AI v SEO (1 deň) - 150€', - datumOd: new Date('2026-02-13'), - datumDo: new Date('2026-02-13'), - pocetUcastnikov: 1, - fakturaVystavena: true, - zaplatene: false, - stav: 'registrovany', - }, -]; - -async function importData() { - console.log('Starting import...'); - - // Create courses (now without dates) - console.log('\nCreating courses...'); - const createdKurzy = {}; - for (const course of coursesData) { - const [created] = await db.insert(kurzy).values({ - nazov: course.nazov, - typKurzu: course.typKurzu, - cena: course.cena, - aktivny: true, - }).returning(); - createdKurzy[course.nazov] = created.id; - console.log(` Created course: ${course.nazov} (ID: ${created.id})`); - } - - // Create participants and registrations (with dates) - console.log('\nCreating participants and registrations...'); - for (const p of participantsData) { - // Check if participant already exists by email - let [existingUcastnik] = await db.select().from(ucastnici).where(eq(ucastnici.email, p.email)).limit(1); - - let ucastnikId; - if (existingUcastnik) { - ucastnikId = existingUcastnik.id; - console.log(` Using existing participant: ${p.email}`); - } else { - const [created] = await db.insert(ucastnici).values({ - titul: p.titul || null, - meno: p.meno, - priezvisko: p.priezvisko, - email: p.email, - telefon: p.telefon || null, - firma: p.firma || null, - mesto: p.mesto || null, - ulica: p.ulica || null, - psc: p.psc || null, - }).returning(); - ucastnikId = created.id; - console.log(` Created participant: ${p.meno} ${p.priezvisko} (${p.email})`); - } - - // Get kurz ID - const kurzId = createdKurzy[p.kurz]; - if (!kurzId) { - console.error(` ERROR: Course not found: ${p.kurz}`); - continue; - } - - // Create registration with dates - await db.insert(registracie).values({ - kurzId: kurzId, - ucastnikId: ucastnikId, - datumOd: p.datumOd || null, - datumDo: p.datumDo || null, - formaKurzu: p.formaKurzu || 'prezencne', - pocetUcastnikov: p.pocetUcastnikov || 1, - fakturaVystavena: p.fakturaVystavena || false, - zaplatene: p.zaplatene || false, - stav: p.stav || 'registrovany', - poznamka: p.poznamka || null, - }); - console.log(` Created registration for ${p.email} -> ${p.kurz} (${p.datumOd?.toLocaleDateString('sk-SK')} - ${p.datumDo?.toLocaleDateString('sk-SK')})`); - } - - console.log('\n=== Import completed ==='); - console.log(`Courses: ${coursesData.length}`); - console.log(`Participants: ${participantsData.length}`); -} - -// Run -clearData() - .then(() => importData()) - .then(() => { - console.log('Done!'); - process.exit(0); - }) - .catch((error) => { - console.error('Import failed:', error); - process.exit(1); - }); diff --git a/src/db/seeds/ai-kurzy-import.seed.js b/src/db/seeds/ai-kurzy-import.seed.js deleted file mode 100644 index 8c7d184..0000000 --- a/src/db/seeds/ai-kurzy-import.seed.js +++ /dev/null @@ -1,292 +0,0 @@ -import dotenv from 'dotenv'; -dotenv.config(); - -import ExcelJS from 'exceljs'; -import { eq, and } from 'drizzle-orm'; -import path from 'path'; - -// Dynamic imports to ensure env is loaded first -const { db } = await import('../../config/database.js'); -const { kurzy, ucastnici, registracie } = await import('../schema.js'); - -const EXCEL_FILE = '/home/richardtekula/Downloads/Copy of AI školenie študenti.xlsx'; - -// Helper to parse dates from various formats -const parseDate = (value) => { - if (!value) return null; - if (value instanceof Date) return value; - if (typeof value === 'number') { - // Excel serial date number - const date = new Date((value - 25569) * 86400 * 1000); - return date; - } - if (typeof value === 'string') { - const parsed = new Date(value); - return isNaN(parsed.getTime()) ? null : parsed; - } - return null; -}; - -// Helper to clean string values -const cleanString = (value) => { - if (value === null || value === undefined) return null; - const str = String(value).trim(); - return str === '' ? null : str; -}; - -// Helper to parse numeric value -const parseNumber = (value) => { - if (value === null || value === undefined) return null; - const num = parseFloat(value); - return isNaN(num) ? null : num; -}; - -// Map stav from Excel to our enum values -const mapStav = (value) => { - if (!value) return 'registrovany'; - const v = String(value).toLowerCase().trim(); - if (v.includes('absolvoval')) return 'absolvoval'; - if (v.includes('potvrden')) return 'potvrdeny'; - if (v.includes('zrusen')) return 'zruseny'; - if (v.includes('potencial')) return 'potencialny'; - return 'registrovany'; -}; - -// Map forma kurzu -const mapForma = (value) => { - if (!value) return 'prezencne'; - const v = String(value).toLowerCase().trim(); - if (v.includes('online')) return 'online'; - if (v.includes('hybrid')) return 'hybridne'; - return 'prezencne'; -}; - -async function importAiKurzy() { - console.log('Reading Excel file:', EXCEL_FILE); - - const workbook = new ExcelJS.Workbook(); - await workbook.xlsx.readFile(EXCEL_FILE); - - console.log('Sheets in workbook:', workbook.worksheets.map(ws => ws.name)); - - // Process each sheet - for (const worksheet of workbook.worksheets) { - console.log(`\n=== Processing sheet: ${worksheet.name} ===`); - console.log(`Rows: ${worksheet.rowCount}, Columns: ${worksheet.columnCount}`); - - // Get headers from first row - const headerRow = worksheet.getRow(1); - const headers = []; - headerRow.eachCell((cell, colNum) => { - headers[colNum] = cleanString(cell.value); - }); - console.log('Headers:', headers.filter(Boolean)); - - // Collect data rows - const dataRows = []; - worksheet.eachRow((row, rowNum) => { - if (rowNum === 1) return; // Skip header - - const rowData = {}; - row.eachCell((cell, colNum) => { - const header = headers[colNum]; - if (header) { - rowData[header] = cell.value; - } - }); - - // Only add if row has some data - if (Object.values(rowData).some(v => v !== null && v !== undefined && v !== '')) { - dataRows.push(rowData); - } - }); - - console.log(`Found ${dataRows.length} data rows`); - - // Log first few rows to understand structure - if (dataRows.length > 0) { - console.log('Sample row:', JSON.stringify(dataRows[0], null, 2)); - } - - // Try to import data based on headers - await importSheetData(worksheet.name, headers, dataRows); - } - - console.log('\n=== Import completed ==='); -} - -async function importSheetData(sheetName, headers, rows) { - // Detect what kind of data this is based on headers - const headerLower = headers.map(h => h?.toLowerCase() || ''); - - const hasKurzFields = headerLower.some(h => h.includes('kurz') || h.includes('datum') || h.includes('cena')); - const hasUcastnikFields = headerLower.some(h => h.includes('meno') || h.includes('email') || h.includes('priezvisko')); - - if (rows.length === 0) { - console.log('No data to import'); - return; - } - - // Import participants and registrations - if (hasUcastnikFields) { - await importParticipantsAndRegistrations(sheetName, headers, rows); - } -} - -async function importParticipantsAndRegistrations(sheetName, headers, rows) { - console.log(`\nImporting participants from sheet: ${sheetName}`); - - // First, ensure we have a course for this sheet - const courseName = sheetName; - let course = await db.select().from(kurzy).where(eq(kurzy.nazov, courseName)).limit(1); - - if (course.length === 0) { - // Create course from sheet name - const [newCourse] = await db.insert(kurzy).values({ - nazov: courseName, - typKurzu: extractCourseType(sheetName), - cena: '0', // Will need to update manually - datumOd: new Date(), - datumDo: new Date(), - aktivny: true, - }).returning(); - course = [newCourse]; - console.log(`Created course: ${courseName} (ID: ${newCourse.id})`); - } else { - console.log(`Using existing course: ${courseName} (ID: ${course[0].id})`); - } - - const kurzId = course[0].id; - - // Map headers to our fields - const headerMap = {}; - headers.forEach((header, idx) => { - if (!header) return; - const h = header.toLowerCase(); - - if (h.includes('titul') || h === 'titul') headerMap.titul = idx; - if (h.includes('meno') && !h.includes('priezvisko')) headerMap.meno = idx; - if (h.includes('priezvisko') || h === 'surname' || h === 'priezvisko') headerMap.priezvisko = idx; - if (h.includes('email') || h.includes('e-mail')) headerMap.email = idx; - if (h.includes('telefon') || h.includes('phone') || h.includes('tel')) headerMap.telefon = idx; - if (h.includes('firma') || h.includes('company') || h.includes('spolocnost')) headerMap.firma = idx; - if (h.includes('mesto') || h.includes('city')) headerMap.mesto = idx; - if (h.includes('ulica') || h.includes('street') || h.includes('adresa')) headerMap.ulica = idx; - if (h.includes('psc') || h.includes('zip') || h.includes('postal')) headerMap.psc = idx; - if (h.includes('stav') || h.includes('status')) headerMap.stav = idx; - if (h.includes('forma') || h.includes('form')) headerMap.forma = idx; - if (h.includes('faktur') && h.includes('cislo')) headerMap.fakturaCislo = idx; - if (h.includes('faktur') && h.includes('vystaven')) headerMap.fakturaVystavena = idx; - if (h.includes('zaplaten') || h.includes('paid')) headerMap.zaplatene = idx; - if (h.includes('poznam') || h.includes('note')) headerMap.poznamka = idx; - if (h.includes('pocet') || h.includes('count')) headerMap.pocetUcastnikov = idx; - }); - - console.log('Field mapping:', headerMap); - - let importedCount = 0; - let skippedCount = 0; - - for (const row of rows) { - try { - // Get email - required field - const email = cleanString(row[headers[headerMap.email]] || Object.values(row).find(v => String(v).includes('@'))); - if (!email || !email.includes('@')) { - skippedCount++; - continue; - } - - // Check if participant exists - let participant = await db.select().from(ucastnici).where(eq(ucastnici.email, email)).limit(1); - - if (participant.length === 0) { - // Try to find name fields - let meno = cleanString(row[headers[headerMap.meno]]); - let priezvisko = cleanString(row[headers[headerMap.priezvisko]]); - - // If no separate fields, try to split full name - if (!meno && !priezvisko) { - // Look for a name-like field - for (const [key, value] of Object.entries(row)) { - const val = cleanString(value); - if (val && !val.includes('@') && !val.includes('http') && val.length < 50) { - const parts = val.split(/\s+/); - if (parts.length >= 2) { - meno = parts[0]; - priezvisko = parts.slice(1).join(' '); - break; - } - } - } - } - - // Create participant - const [newParticipant] = await db.insert(ucastnici).values({ - titul: cleanString(row[headers[headerMap.titul]]), - meno: meno || 'N/A', - priezvisko: priezvisko || 'N/A', - email: email, - telefon: cleanString(row[headers[headerMap.telefon]]), - firma: cleanString(row[headers[headerMap.firma]]), - mesto: cleanString(row[headers[headerMap.mesto]]), - ulica: cleanString(row[headers[headerMap.ulica]]), - psc: cleanString(row[headers[headerMap.psc]]), - }).returning(); - participant = [newParticipant]; - console.log(`Created participant: ${email}`); - } - - const ucastnikId = participant[0].id; - - // Check if registration exists - const existingReg = await db.select() - .from(registracie) - .where(and(eq(registracie.kurzId, kurzId), eq(registracie.ucastnikId, ucastnikId))) - .limit(1); - - if (existingReg.length === 0) { - // Create registration - await db.insert(registracie).values({ - kurzId: kurzId, - ucastnikId: ucastnikId, - formaKurzu: mapForma(row[headers[headerMap.forma]]), - pocetUcastnikov: parseInt(row[headers[headerMap.pocetUcastnikov]]) || 1, - fakturaCislo: cleanString(row[headers[headerMap.fakturaCislo]]), - fakturaVystavena: Boolean(row[headers[headerMap.fakturaVystavena]]), - zaplatene: Boolean(row[headers[headerMap.zaplatene]]), - stav: mapStav(row[headers[headerMap.stav]]), - poznamka: cleanString(row[headers[headerMap.poznamka]]), - }); - importedCount++; - } else { - console.log(`Registration already exists for ${email} in ${sheetName}`); - skippedCount++; - } - } catch (error) { - console.error(`Error processing row:`, error.message); - skippedCount++; - } - } - - console.log(`Imported ${importedCount} registrations, skipped ${skippedCount}`); -} - -function extractCourseType(sheetName) { - const name = sheetName.toLowerCase(); - if (name.includes('ai 1') || name.includes('ai1')) return 'AI 1'; - if (name.includes('ai 2') || name.includes('ai2')) return 'AI 2'; - if (name.includes('seo')) return 'SEO'; - if (name.includes('marketing')) return 'Marketing'; - return 'AI'; -} - -// Run the import -importAiKurzy() - .then(() => { - console.log('Import finished successfully'); - process.exit(0); - }) - .catch((error) => { - console.error('Import failed:', error); - process.exit(1); - });