Files
crm-server/ai-kurzy-tables.md
richardtekula 4089bb4be2 feat: AI Kurzy module, project/service documents, services SQL import
- Add AI Kurzy module with courses, participants, and registrations management
- Add project documents and service documents features
- Add service folders for document organization
- Add SQL import queries for services from firmy.slovensko.ai
- Update todo notifications and group messaging
- Various API improvements and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 11:32:49 +01:00

8.9 KiB

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

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

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)

// Jeden kurz má VIAC registrácií
kurzyRelations = relations(kurzy, ({ many }) => ({
  registracie: many(registracie),
}));

N:1 (Many-to-One)

// 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

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