- 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>
329 lines
8.9 KiB
Markdown
329 lines
8.9 KiB
Markdown
# 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
|