refactor: Move course dates from registracie to kurzy table

- Add datumOd and datumDo columns to kurzy table
- Remove datumOd, datumDo, pocetUcastnikov from registracie table
- Update schema, validators, and services accordingly
- Certificate generation now uses course dates
- Migration preserves existing data by copying most recent dates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-29 12:52:56 +01:00
parent 2fee1f39bc
commit 12acd68156
6 changed files with 49 additions and 36 deletions

View File

@@ -0,0 +1,21 @@
-- Migration: Move dates from registracie to kurzy table
-- Step 1: Add new columns to kurzy table
ALTER TABLE kurzy ADD COLUMN IF NOT EXISTS datum_od DATE;
ALTER TABLE kurzy ADD COLUMN IF NOT EXISTS datum_do DATE;
-- Step 2: Migrate existing data - copy most recent registration dates to each course
UPDATE kurzy k
SET datum_od = r.datum_od, datum_do = r.datum_do
FROM (
SELECT DISTINCT ON (kurz_id) kurz_id, datum_od, datum_do
FROM registracie
WHERE datum_od IS NOT NULL OR datum_do IS NOT NULL
ORDER BY kurz_id, created_at DESC
) r
WHERE k.id = r.kurz_id;
-- Step 3: Drop columns from registracie table
ALTER TABLE registracie DROP COLUMN IF EXISTS datum_od;
ALTER TABLE registracie DROP COLUMN IF EXISTS datum_do;
ALTER TABLE registracie DROP COLUMN IF EXISTS pocet_ucastnikov;

View File

@@ -441,6 +441,8 @@ export const kurzy = pgTable('kurzy', {
maxKapacita: integer('max_kapacita'),
aktivny: boolean('aktivny').default(true).notNull(),
farba: varchar('farba', { length: 20 }), // Color for visual distinction (e.g., 'primary', 'info', 'warning')
datumOd: date('datum_od', { mode: 'date' }),
datumDo: date('datum_do', { mode: 'date' }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
@@ -473,10 +475,7 @@ export const registracie = pgTable('registracie', {
id: serial('id').primaryKey(),
kurzId: integer('kurz_id').notNull().references(() => kurzy.id, { onDelete: 'cascade' }),
ucastnikId: integer('ucastnik_id').notNull().references(() => ucastnici.id, { onDelete: 'cascade' }),
datumOd: date('datum_od', { mode: 'date' }), // dátum začiatku pre túto registráciu
datumDo: date('datum_do', { mode: 'date' }), // dátum konca pre túto registráciu
formaKurzu: formaKurzuEnum('forma_kurzu').default('prezencne').notNull(),
pocetUcastnikov: integer('pocet_ucastnikov').default(1).notNull(),
fakturaCislo: varchar('faktura_cislo', { length: 100 }),
fakturaVystavena: boolean('faktura_vystavena').default(false).notNull(),
zaplatene: boolean('zaplatene').default(false).notNull(),

View File

@@ -151,8 +151,6 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
const [registration] = await db
.select({
id: registracie.id,
datumOd: registracie.datumOd,
datumDo: registracie.datumDo,
ucastnikId: registracie.ucastnikId,
kurzId: registracie.kurzId,
})
@@ -184,6 +182,8 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
.select({
nazov: kurzy.nazov,
popis: kurzy.popis,
datumOd: kurzy.datumOd,
datumDo: kurzy.datumDo,
})
.from(kurzy)
.where(eq(kurzy.id, registration.kurzId))
@@ -205,16 +205,16 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
.filter(Boolean)
.join(' ');
const issueDate = registration.datumDo || registration.datumOd || new Date();
const issueDate = course.datumDo || course.datumOd || new Date();
// Format date range
let dateRange = '';
if (registration.datumOd && registration.datumDo) {
dateRange = `${formatDate(registration.datumOd)} - ${formatDate(registration.datumDo)}`;
} else if (registration.datumDo) {
dateRange = formatDate(registration.datumDo);
} else if (registration.datumOd) {
dateRange = formatDate(registration.datumOd);
if (course.datumOd && course.datumDo) {
dateRange = `${formatDate(course.datumOd)} - ${formatDate(course.datumDo)}`;
} else if (course.datumDo) {
dateRange = formatDate(course.datumDo);
} else if (course.datumOd) {
dateRange = formatDate(course.datumOd);
}
// Prepare template data

View File

@@ -14,6 +14,8 @@ export const getAllKurzy = async () => {
maxKapacita: kurzy.maxKapacita,
aktivny: kurzy.aktivny,
farba: kurzy.farba,
datumOd: kurzy.datumOd,
datumDo: kurzy.datumDo,
createdAt: kurzy.createdAt,
registraciiCount: sql`(SELECT COUNT(*) FROM registracie WHERE kurz_id = ${kurzy.id})::int`,
})
@@ -48,6 +50,8 @@ export const createKurz = async (data) => {
maxKapacita: data.maxKapacita || null,
aktivny: data.aktivny !== undefined ? data.aktivny : true,
farba: data.farba || null,
datumOd: data.datumOd ? new Date(data.datumOd) : null,
datumDo: data.datumDo ? new Date(data.datumDo) : null,
})
.returning();
@@ -65,6 +69,8 @@ export const updateKurz = async (id, data) => {
maxKapacita: data.maxKapacita !== undefined ? data.maxKapacita : undefined,
aktivny: data.aktivny !== undefined ? data.aktivny : undefined,
farba: data.farba !== undefined ? data.farba : undefined,
datumOd: data.datumOd !== undefined ? (data.datumOd ? new Date(data.datumOd) : null) : undefined,
datumDo: data.datumDo !== undefined ? (data.datumDo ? new Date(data.datumDo) : null) : undefined,
updatedAt: new Date(),
};

View File

@@ -11,10 +11,7 @@ export const getAllRegistracie = async (kurzId = null) => {
id: registracie.id,
kurzId: registracie.kurzId,
ucastnikId: registracie.ucastnikId,
datumOd: registracie.datumOd,
datumDo: registracie.datumDo,
formaKurzu: registracie.formaKurzu,
pocetUcastnikov: registracie.pocetUcastnikov,
fakturaCislo: registracie.fakturaCislo,
fakturaVystavena: registracie.fakturaVystavena,
zaplatene: registracie.zaplatene,
@@ -23,6 +20,8 @@ export const getAllRegistracie = async (kurzId = null) => {
createdAt: registracie.createdAt,
kurzNazov: kurzy.nazov,
kurzTyp: kurzy.typKurzu,
kurzDatumOd: kurzy.datumOd,
kurzDatumDo: kurzy.datumDo,
ucastnikMeno: ucastnici.meno,
ucastnikPriezvisko: ucastnici.priezvisko,
ucastnikEmail: ucastnici.email,
@@ -32,7 +31,7 @@ export const getAllRegistracie = async (kurzId = null) => {
.leftJoin(kurzy, eq(registracie.kurzId, kurzy.id))
.leftJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(registracie.datumOd), desc(registracie.createdAt));
.orderBy(desc(kurzy.datumOd), desc(registracie.createdAt));
return result;
};
@@ -44,7 +43,6 @@ export const getRegistraciaById = async (id) => {
kurzId: registracie.kurzId,
ucastnikId: registracie.ucastnikId,
formaKurzu: registracie.formaKurzu,
pocetUcastnikov: registracie.pocetUcastnikov,
fakturaCislo: registracie.fakturaCislo,
fakturaVystavena: registracie.fakturaVystavena,
zaplatene: registracie.zaplatene,
@@ -76,10 +74,7 @@ export const createRegistracia = async (data) => {
.values({
kurzId: data.kurzId,
ucastnikId: data.ucastnikId,
datumOd: data.datumOd ? new Date(data.datumOd) : null,
datumDo: data.datumDo ? new Date(data.datumDo) : null,
formaKurzu: data.formaKurzu || 'prezencne',
pocetUcastnikov: data.pocetUcastnikov || 1,
fakturaCislo: data.fakturaCislo || null,
fakturaVystavena: data.fakturaVystavena || false,
zaplatene: data.zaplatene || false,
@@ -135,10 +130,9 @@ export const getCombinedTableData = async () => {
kurzNazov: kurzy.nazov,
kurzTyp: kurzy.typKurzu,
kurzFarba: kurzy.farba,
datumOd: registracie.datumOd,
datumDo: registracie.datumDo,
datumOd: kurzy.datumOd,
datumDo: kurzy.datumDo,
formaKurzu: registracie.formaKurzu,
pocetUcastnikov: registracie.pocetUcastnikov,
fakturaCislo: registracie.fakturaCislo,
fakturaVystavena: registracie.fakturaVystavena,
zaplatene: registracie.zaplatene,
@@ -151,15 +145,14 @@ export const getCombinedTableData = async () => {
.from(registracie)
.innerJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
.innerJoin(kurzy, eq(registracie.kurzId, kurzy.id))
.orderBy(desc(registracie.datumOd), desc(registracie.createdAt));
.orderBy(desc(kurzy.datumOd), desc(registracie.createdAt));
return result;
};
export const updateField = async (registrationId, field, value) => {
const ucastnikFields = ['titul', 'meno', 'priezvisko', 'email', 'telefon', 'firma', 'firmaIco', 'firmaDic', 'firmaIcDph', 'firmaSidlo', 'mesto', 'ulica', 'psc', 'needsFollowup'];
const registraciaFields = ['datumOd', 'datumDo', 'formaKurzu', 'pocetUcastnikov', 'fakturaCislo', 'fakturaVystavena', 'zaplatene', 'stav', 'poznamka', 'kurzId'];
const dateFields = ['datumOd', 'datumDo'];
const registraciaFields = ['formaKurzu', 'fakturaCislo', 'fakturaVystavena', 'zaplatene', 'stav', 'poznamka', 'kurzId'];
const [reg] = await db
.select({ ucastnikId: registracie.ucastnikId })
@@ -171,20 +164,15 @@ export const updateField = async (registrationId, field, value) => {
throw new NotFoundError('Registrácia nenájdená');
}
let processedValue = value;
if (dateFields.includes(field)) {
processedValue = value ? new Date(value) : null;
}
if (ucastnikFields.includes(field)) {
await db
.update(ucastnici)
.set({ [field]: processedValue, updatedAt: new Date() })
.set({ [field]: value, updatedAt: new Date() })
.where(eq(ucastnici.id, reg.ucastnikId));
} else if (registraciaFields.includes(field)) {
await db
.update(registracie)
.set({ [field]: processedValue, updatedAt: new Date() })
.set({ [field]: value, updatedAt: new Date() })
.where(eq(registracie.id, registrationId));
} else {
throw new Error(`Unknown field: ${field}`);

View File

@@ -20,6 +20,8 @@ export const createKurzSchema = z.object({
maxKapacita: z.number().int().positive().optional().nullable(),
aktivny: z.boolean().optional(),
farba: z.string().max(20).optional().nullable(),
datumOd: z.string().optional().nullable(),
datumDo: z.string().optional().nullable(),
});
export const updateKurzSchema = createKurzSchema.partial();
@@ -45,10 +47,7 @@ export const updateUcastnikSchema = createUcastnikSchema.partial();
export const createRegistraciaSchema = z.object({
kurzId: z.number().int().positive(),
ucastnikId: z.number().int().positive(),
datumOd: z.string().optional().nullable(),
datumDo: z.string().optional().nullable(),
formaKurzu: z.enum(['prezencne', 'online', 'hybridne']).optional(),
pocetUcastnikov: z.number().int().positive().optional(),
fakturaCislo: z.string().max(100).optional().nullable(),
fakturaVystavena: z.boolean().optional(),
zaplatene: z.boolean().optional(),