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>
This commit is contained in:
328
ai-kurzy-tables.md
Normal file
328
ai-kurzy-tables.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user