- 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>
8.9 KiB
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ínovucastnici- osobné údaje účastníkovregistracie- 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