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:
1550
DOCUMENTATION.md
1550
DOCUMENTATION.md
File diff suppressed because it is too large
Load Diff
139
README.md
139
README.md
@@ -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.
|
||||
140
SQL_QUERIES.txt
140
SQL_QUERIES.txt
@@ -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
|
||||
================================================================================
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
34
sql-fix.txt
34
sql-fix.txt
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user