chore: Commit current state before refactoring

Includes deleted sql/ files, seeds, and documentation files.
Prepares master for refactoring branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-28 07:19:23 +01:00
parent 95688be45b
commit 883d3fa533
14 changed files with 1 additions and 3398 deletions

File diff suppressed because it is too large Load Diff

139
README.md
View File

@@ -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 10MB), `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 5MB, 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.

View File

@@ -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
================================================================================

View File

@@ -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<typeof kurzy>;
export type Ucastnik = InferSelectModel<typeof ucastnici>;
export type Registracia = InferSelectModel<typeof registracie>;
export type Priloha = InferSelectModel<typeof prilohy>;
// INSERT typy (vkladanie do DB)
export type NewKurz = InferInsertModel<typeof kurzy>;
export type NewUcastnik = InferInsertModel<typeof ucastnici>;
export type NewRegistracia = InferInsertModel<typeof registracie>;
export type NewPriloha = InferInsertModel<typeof prilohy>;
```
---
## 🔗 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

View File

@@ -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",

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <postgres_container_id> psql -U <username> -d <database>
# Alebo ak mas pristup cez SSH:
psql -h localhost -U <username> -d <database>
```
### 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!

View File

@@ -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);
});

View File

@@ -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);
});