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:
21
src/db/migrations/0011_refactor_ai_kurzy_dates.sql
Normal file
21
src/db/migrations/0011_refactor_ai_kurzy_dates.sql
Normal 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;
|
||||||
@@ -441,6 +441,8 @@ export const kurzy = pgTable('kurzy', {
|
|||||||
maxKapacita: integer('max_kapacita'),
|
maxKapacita: integer('max_kapacita'),
|
||||||
aktivny: boolean('aktivny').default(true).notNull(),
|
aktivny: boolean('aktivny').default(true).notNull(),
|
||||||
farba: varchar('farba', { length: 20 }), // Color for visual distinction (e.g., 'primary', 'info', 'warning')
|
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(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_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(),
|
id: serial('id').primaryKey(),
|
||||||
kurzId: integer('kurz_id').notNull().references(() => kurzy.id, { onDelete: 'cascade' }),
|
kurzId: integer('kurz_id').notNull().references(() => kurzy.id, { onDelete: 'cascade' }),
|
||||||
ucastnikId: integer('ucastnik_id').notNull().references(() => ucastnici.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(),
|
formaKurzu: formaKurzuEnum('forma_kurzu').default('prezencne').notNull(),
|
||||||
pocetUcastnikov: integer('pocet_ucastnikov').default(1).notNull(),
|
|
||||||
fakturaCislo: varchar('faktura_cislo', { length: 100 }),
|
fakturaCislo: varchar('faktura_cislo', { length: 100 }),
|
||||||
fakturaVystavena: boolean('faktura_vystavena').default(false).notNull(),
|
fakturaVystavena: boolean('faktura_vystavena').default(false).notNull(),
|
||||||
zaplatene: boolean('zaplatene').default(false).notNull(),
|
zaplatene: boolean('zaplatene').default(false).notNull(),
|
||||||
|
|||||||
@@ -151,8 +151,6 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
|
|||||||
const [registration] = await db
|
const [registration] = await db
|
||||||
.select({
|
.select({
|
||||||
id: registracie.id,
|
id: registracie.id,
|
||||||
datumOd: registracie.datumOd,
|
|
||||||
datumDo: registracie.datumDo,
|
|
||||||
ucastnikId: registracie.ucastnikId,
|
ucastnikId: registracie.ucastnikId,
|
||||||
kurzId: registracie.kurzId,
|
kurzId: registracie.kurzId,
|
||||||
})
|
})
|
||||||
@@ -184,6 +182,8 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
|
|||||||
.select({
|
.select({
|
||||||
nazov: kurzy.nazov,
|
nazov: kurzy.nazov,
|
||||||
popis: kurzy.popis,
|
popis: kurzy.popis,
|
||||||
|
datumOd: kurzy.datumOd,
|
||||||
|
datumDo: kurzy.datumDo,
|
||||||
})
|
})
|
||||||
.from(kurzy)
|
.from(kurzy)
|
||||||
.where(eq(kurzy.id, registration.kurzId))
|
.where(eq(kurzy.id, registration.kurzId))
|
||||||
@@ -205,16 +205,16 @@ export const generateCertificate = async (registraciaId, templateName = 'AIcerti
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const issueDate = registration.datumDo || registration.datumOd || new Date();
|
const issueDate = course.datumDo || course.datumOd || new Date();
|
||||||
|
|
||||||
// Format date range
|
// Format date range
|
||||||
let dateRange = '';
|
let dateRange = '';
|
||||||
if (registration.datumOd && registration.datumDo) {
|
if (course.datumOd && course.datumDo) {
|
||||||
dateRange = `${formatDate(registration.datumOd)} - ${formatDate(registration.datumDo)}`;
|
dateRange = `${formatDate(course.datumOd)} - ${formatDate(course.datumDo)}`;
|
||||||
} else if (registration.datumDo) {
|
} else if (course.datumDo) {
|
||||||
dateRange = formatDate(registration.datumDo);
|
dateRange = formatDate(course.datumDo);
|
||||||
} else if (registration.datumOd) {
|
} else if (course.datumOd) {
|
||||||
dateRange = formatDate(registration.datumOd);
|
dateRange = formatDate(course.datumOd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare template data
|
// Prepare template data
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const getAllKurzy = async () => {
|
|||||||
maxKapacita: kurzy.maxKapacita,
|
maxKapacita: kurzy.maxKapacita,
|
||||||
aktivny: kurzy.aktivny,
|
aktivny: kurzy.aktivny,
|
||||||
farba: kurzy.farba,
|
farba: kurzy.farba,
|
||||||
|
datumOd: kurzy.datumOd,
|
||||||
|
datumDo: kurzy.datumDo,
|
||||||
createdAt: kurzy.createdAt,
|
createdAt: kurzy.createdAt,
|
||||||
registraciiCount: sql`(SELECT COUNT(*) FROM registracie WHERE kurz_id = ${kurzy.id})::int`,
|
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,
|
maxKapacita: data.maxKapacita || null,
|
||||||
aktivny: data.aktivny !== undefined ? data.aktivny : true,
|
aktivny: data.aktivny !== undefined ? data.aktivny : true,
|
||||||
farba: data.farba || null,
|
farba: data.farba || null,
|
||||||
|
datumOd: data.datumOd ? new Date(data.datumOd) : null,
|
||||||
|
datumDo: data.datumDo ? new Date(data.datumDo) : null,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -65,6 +69,8 @@ export const updateKurz = async (id, data) => {
|
|||||||
maxKapacita: data.maxKapacita !== undefined ? data.maxKapacita : undefined,
|
maxKapacita: data.maxKapacita !== undefined ? data.maxKapacita : undefined,
|
||||||
aktivny: data.aktivny !== undefined ? data.aktivny : undefined,
|
aktivny: data.aktivny !== undefined ? data.aktivny : undefined,
|
||||||
farba: data.farba !== undefined ? data.farba : 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(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ export const getAllRegistracie = async (kurzId = null) => {
|
|||||||
id: registracie.id,
|
id: registracie.id,
|
||||||
kurzId: registracie.kurzId,
|
kurzId: registracie.kurzId,
|
||||||
ucastnikId: registracie.ucastnikId,
|
ucastnikId: registracie.ucastnikId,
|
||||||
datumOd: registracie.datumOd,
|
|
||||||
datumDo: registracie.datumDo,
|
|
||||||
formaKurzu: registracie.formaKurzu,
|
formaKurzu: registracie.formaKurzu,
|
||||||
pocetUcastnikov: registracie.pocetUcastnikov,
|
|
||||||
fakturaCislo: registracie.fakturaCislo,
|
fakturaCislo: registracie.fakturaCislo,
|
||||||
fakturaVystavena: registracie.fakturaVystavena,
|
fakturaVystavena: registracie.fakturaVystavena,
|
||||||
zaplatene: registracie.zaplatene,
|
zaplatene: registracie.zaplatene,
|
||||||
@@ -23,6 +20,8 @@ export const getAllRegistracie = async (kurzId = null) => {
|
|||||||
createdAt: registracie.createdAt,
|
createdAt: registracie.createdAt,
|
||||||
kurzNazov: kurzy.nazov,
|
kurzNazov: kurzy.nazov,
|
||||||
kurzTyp: kurzy.typKurzu,
|
kurzTyp: kurzy.typKurzu,
|
||||||
|
kurzDatumOd: kurzy.datumOd,
|
||||||
|
kurzDatumDo: kurzy.datumDo,
|
||||||
ucastnikMeno: ucastnici.meno,
|
ucastnikMeno: ucastnici.meno,
|
||||||
ucastnikPriezvisko: ucastnici.priezvisko,
|
ucastnikPriezvisko: ucastnici.priezvisko,
|
||||||
ucastnikEmail: ucastnici.email,
|
ucastnikEmail: ucastnici.email,
|
||||||
@@ -32,7 +31,7 @@ export const getAllRegistracie = async (kurzId = null) => {
|
|||||||
.leftJoin(kurzy, eq(registracie.kurzId, kurzy.id))
|
.leftJoin(kurzy, eq(registracie.kurzId, kurzy.id))
|
||||||
.leftJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
|
.leftJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
|
||||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||||
.orderBy(desc(registracie.datumOd), desc(registracie.createdAt));
|
.orderBy(desc(kurzy.datumOd), desc(registracie.createdAt));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -44,7 +43,6 @@ export const getRegistraciaById = async (id) => {
|
|||||||
kurzId: registracie.kurzId,
|
kurzId: registracie.kurzId,
|
||||||
ucastnikId: registracie.ucastnikId,
|
ucastnikId: registracie.ucastnikId,
|
||||||
formaKurzu: registracie.formaKurzu,
|
formaKurzu: registracie.formaKurzu,
|
||||||
pocetUcastnikov: registracie.pocetUcastnikov,
|
|
||||||
fakturaCislo: registracie.fakturaCislo,
|
fakturaCislo: registracie.fakturaCislo,
|
||||||
fakturaVystavena: registracie.fakturaVystavena,
|
fakturaVystavena: registracie.fakturaVystavena,
|
||||||
zaplatene: registracie.zaplatene,
|
zaplatene: registracie.zaplatene,
|
||||||
@@ -76,10 +74,7 @@ export const createRegistracia = async (data) => {
|
|||||||
.values({
|
.values({
|
||||||
kurzId: data.kurzId,
|
kurzId: data.kurzId,
|
||||||
ucastnikId: data.ucastnikId,
|
ucastnikId: data.ucastnikId,
|
||||||
datumOd: data.datumOd ? new Date(data.datumOd) : null,
|
|
||||||
datumDo: data.datumDo ? new Date(data.datumDo) : null,
|
|
||||||
formaKurzu: data.formaKurzu || 'prezencne',
|
formaKurzu: data.formaKurzu || 'prezencne',
|
||||||
pocetUcastnikov: data.pocetUcastnikov || 1,
|
|
||||||
fakturaCislo: data.fakturaCislo || null,
|
fakturaCislo: data.fakturaCislo || null,
|
||||||
fakturaVystavena: data.fakturaVystavena || false,
|
fakturaVystavena: data.fakturaVystavena || false,
|
||||||
zaplatene: data.zaplatene || false,
|
zaplatene: data.zaplatene || false,
|
||||||
@@ -135,10 +130,9 @@ export const getCombinedTableData = async () => {
|
|||||||
kurzNazov: kurzy.nazov,
|
kurzNazov: kurzy.nazov,
|
||||||
kurzTyp: kurzy.typKurzu,
|
kurzTyp: kurzy.typKurzu,
|
||||||
kurzFarba: kurzy.farba,
|
kurzFarba: kurzy.farba,
|
||||||
datumOd: registracie.datumOd,
|
datumOd: kurzy.datumOd,
|
||||||
datumDo: registracie.datumDo,
|
datumDo: kurzy.datumDo,
|
||||||
formaKurzu: registracie.formaKurzu,
|
formaKurzu: registracie.formaKurzu,
|
||||||
pocetUcastnikov: registracie.pocetUcastnikov,
|
|
||||||
fakturaCislo: registracie.fakturaCislo,
|
fakturaCislo: registracie.fakturaCislo,
|
||||||
fakturaVystavena: registracie.fakturaVystavena,
|
fakturaVystavena: registracie.fakturaVystavena,
|
||||||
zaplatene: registracie.zaplatene,
|
zaplatene: registracie.zaplatene,
|
||||||
@@ -151,15 +145,14 @@ export const getCombinedTableData = async () => {
|
|||||||
.from(registracie)
|
.from(registracie)
|
||||||
.innerJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
|
.innerJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
|
||||||
.innerJoin(kurzy, eq(registracie.kurzId, kurzy.id))
|
.innerJoin(kurzy, eq(registracie.kurzId, kurzy.id))
|
||||||
.orderBy(desc(registracie.datumOd), desc(registracie.createdAt));
|
.orderBy(desc(kurzy.datumOd), desc(registracie.createdAt));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateField = async (registrationId, field, value) => {
|
export const updateField = async (registrationId, field, value) => {
|
||||||
const ucastnikFields = ['titul', 'meno', 'priezvisko', 'email', 'telefon', 'firma', 'firmaIco', 'firmaDic', 'firmaIcDph', 'firmaSidlo', 'mesto', 'ulica', 'psc', 'needsFollowup'];
|
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 registraciaFields = ['formaKurzu', 'fakturaCislo', 'fakturaVystavena', 'zaplatene', 'stav', 'poznamka', 'kurzId'];
|
||||||
const dateFields = ['datumOd', 'datumDo'];
|
|
||||||
|
|
||||||
const [reg] = await db
|
const [reg] = await db
|
||||||
.select({ ucastnikId: registracie.ucastnikId })
|
.select({ ucastnikId: registracie.ucastnikId })
|
||||||
@@ -171,20 +164,15 @@ export const updateField = async (registrationId, field, value) => {
|
|||||||
throw new NotFoundError('Registrácia nenájdená');
|
throw new NotFoundError('Registrácia nenájdená');
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedValue = value;
|
|
||||||
if (dateFields.includes(field)) {
|
|
||||||
processedValue = value ? new Date(value) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ucastnikFields.includes(field)) {
|
if (ucastnikFields.includes(field)) {
|
||||||
await db
|
await db
|
||||||
.update(ucastnici)
|
.update(ucastnici)
|
||||||
.set({ [field]: processedValue, updatedAt: new Date() })
|
.set({ [field]: value, updatedAt: new Date() })
|
||||||
.where(eq(ucastnici.id, reg.ucastnikId));
|
.where(eq(ucastnici.id, reg.ucastnikId));
|
||||||
} else if (registraciaFields.includes(field)) {
|
} else if (registraciaFields.includes(field)) {
|
||||||
await db
|
await db
|
||||||
.update(registracie)
|
.update(registracie)
|
||||||
.set({ [field]: processedValue, updatedAt: new Date() })
|
.set({ [field]: value, updatedAt: new Date() })
|
||||||
.where(eq(registracie.id, registrationId));
|
.where(eq(registracie.id, registrationId));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown field: ${field}`);
|
throw new Error(`Unknown field: ${field}`);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const createKurzSchema = z.object({
|
|||||||
maxKapacita: z.number().int().positive().optional().nullable(),
|
maxKapacita: z.number().int().positive().optional().nullable(),
|
||||||
aktivny: z.boolean().optional(),
|
aktivny: z.boolean().optional(),
|
||||||
farba: z.string().max(20).optional().nullable(),
|
farba: z.string().max(20).optional().nullable(),
|
||||||
|
datumOd: z.string().optional().nullable(),
|
||||||
|
datumDo: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateKurzSchema = createKurzSchema.partial();
|
export const updateKurzSchema = createKurzSchema.partial();
|
||||||
@@ -45,10 +47,7 @@ export const updateUcastnikSchema = createUcastnikSchema.partial();
|
|||||||
export const createRegistraciaSchema = z.object({
|
export const createRegistraciaSchema = z.object({
|
||||||
kurzId: z.number().int().positive(),
|
kurzId: z.number().int().positive(),
|
||||||
ucastnikId: 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(),
|
formaKurzu: z.enum(['prezencne', 'online', 'hybridne']).optional(),
|
||||||
pocetUcastnikov: z.number().int().positive().optional(),
|
|
||||||
fakturaCislo: z.string().max(100).optional().nullable(),
|
fakturaCislo: z.string().max(100).optional().nullable(),
|
||||||
fakturaVystavena: z.boolean().optional(),
|
fakturaVystavena: z.boolean().optional(),
|
||||||
zaplatene: z.boolean().optional(),
|
zaplatene: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user