diff --git a/src/controllers/ai-kurzy.controller.js b/src/controllers/ai-kurzy.controller.js
index d000db9..4c6b590 100644
--- a/src/controllers/ai-kurzy.controller.js
+++ b/src/controllers/ai-kurzy.controller.js
@@ -269,9 +269,10 @@ export const getStats = async (req, res, next) => {
export const generateCertificate = async (req, res, next) => {
try {
const { registraciaId } = req.params;
+ const { templateName } = req.body;
const { generateCertificate: generateCert } = await import('../services/ai-kurzy/certificate.service.js');
- const result = await generateCert(parseInt(registraciaId));
+ const result = await generateCert(parseInt(registraciaId), templateName || 'AIcertifikat');
res.status(201).json({
data: result,
diff --git a/src/services/ai-kurzy/certificate.service.js b/src/services/ai-kurzy/certificate.service.js
index 8dcc5eb..17d98df 100644
--- a/src/services/ai-kurzy/certificate.service.js
+++ b/src/services/ai-kurzy/certificate.service.js
@@ -4,12 +4,30 @@ import fs from 'fs/promises';
import crypto from 'crypto';
import { db } from '../../config/database.js';
import { registracie, ucastnici, kurzy, prilohy } from '../../db/schema.js';
-import { eq } from 'drizzle-orm';
+import { eq, and } from 'drizzle-orm';
import { NotFoundError } from '../../utils/errors.js';
import { logger } from '../../utils/logger.js';
const UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'ai-kurzy', 'certificates');
const ASSETS_DIR = path.join(process.cwd(), 'src', 'assets', 'certificate');
+const TEMPLATES_DIR = path.join(process.cwd(), 'src', 'templates', 'certificates');
+
+/**
+ * Available certificate templates
+ */
+export const CERTIFICATE_TEMPLATES = {
+ AIcertifikat: {
+ name: 'AI Certifikát',
+ file: 'AIcertifikat.html',
+ description: 'Osvedčenie o absolvovaní AI kurzu',
+ },
+ // Add more templates here in the future
+ // participation: {
+ // name: 'Potvrdenie o účasti',
+ // file: 'participation.html',
+ // description: 'Potvrdenie o účasti na školení',
+ // },
+};
/**
* Format date to Slovak format (DD.MM.YYYY)
@@ -47,246 +65,88 @@ const loadAssets = async () => {
]);
return {
- background: `data:image/jpeg;base64,${background.toString('base64')}`,
+ backgroundImage: `data:image/jpeg;base64,${background.toString('base64')}`,
signatureGablasova: `data:image/png;base64,${signatureGablasova.toString('base64')}`,
signatureZdarilek: `data:image/png;base64,${signatureZdarilek.toString('base64')}`,
};
};
/**
- * Generate HTML template for certificate
+ * Load HTML template from file
*/
-const generateCertificateHtml = (data, assets) => {
- const { participantName, courseTitle, courseModules, dateFrom, dateTo, issueDate, certificateId } = data;
-
- // Format date range
- let dateRangeText = '';
- if (dateFrom && dateTo) {
- dateRangeText = `${formatDate(dateFrom)} - ${formatDate(dateTo)}`;
- } else if (dateTo) {
- dateRangeText = formatDate(dateTo);
- } else if (dateFrom) {
- dateRangeText = formatDate(dateFrom);
+const loadTemplate = async (templateName) => {
+ const template = CERTIFICATE_TEMPLATES[templateName];
+ if (!template) {
+ throw new NotFoundError(`Šablóna "${templateName}" neexistuje`);
}
- return `
-
-
-
-
-
- Osvedčenie - ${participantName}
-
-
-
-
-
-
O S V E D Č E N I E
-
o absolvovaní kurzu
-
-
-
${courseTitle}
- ${courseModules ? `
${courseModules}
` : ''}
- ${dateRangeText ? `
${dateRangeText}
` : ''}
-
-
-
${participantName}
-
-
-
-
-
ID: ${certificateId}
-
-
-
-`;
+ return !!existing;
};
/**
* Generate certificate PDF for a registration
+ * @param {number} registraciaId - Registration ID
+ * @param {string} templateName - Template name (default: 'AIcertifikat')
*/
-export const generateCertificate = async (registraciaId) => {
+export const generateCertificate = async (registraciaId, templateName = 'AIcertifikat') => {
+ // Check if certificate already exists
+ const exists = await hasCertificate(registraciaId);
+ if (exists) {
+ throw new Error('Certifikát pre túto registráciu už existuje');
+ }
+
// Fetch registration with participant and course data
const [registration] = await db
.select({
@@ -333,8 +193,11 @@ export const generateCertificate = async (registraciaId) => {
throw new NotFoundError('Kurz nenájdený');
}
- // Load assets
- const assets = await loadAssets();
+ // Load template and assets
+ const [templateHtml, assets] = await Promise.all([
+ loadTemplate(templateName),
+ loadAssets(),
+ ]);
// Generate certificate data
const certificateId = generateCertificateId(registraciaId);
@@ -344,18 +207,29 @@ export const generateCertificate = async (registraciaId) => {
const issueDate = registration.datumDo || registration.datumOd || new Date();
- const certificateData = {
+ // 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);
+ }
+
+ // Prepare template data
+ const templateData = {
participantName,
courseTitle: course.nazov,
- courseModules: course.popis || null,
- dateFrom: registration.datumOd,
- dateTo: registration.datumDo,
- issueDate,
+ courseModules: course.popis || '',
+ dateRange,
+ issueDate: formatDate(issueDate),
certificateId,
+ ...assets,
};
- // Generate HTML
- const html = generateCertificateHtml(certificateData, assets);
+ // Render template with data
+ const html = renderTemplate(templateHtml, templateData);
// Ensure upload directory exists
await fs.mkdir(UPLOAD_DIR, { recursive: true });
@@ -388,7 +262,7 @@ export const generateCertificate = async (registraciaId) => {
margin: { top: 0, right: 0, bottom: 0, left: 0 },
});
- logger.info(`Certificate generated: ${fileName}`);
+ logger.info(`Certificate generated: ${fileName} (template: ${templateName})`);
} finally {
if (browser) {
await browser.close();
@@ -408,7 +282,7 @@ export const generateCertificate = async (registraciaId) => {
cestaKSuboru: filePath,
mimeType: 'application/pdf',
velkostSuboru: stats.size,
- popis: `Certifikát vygenerovaný ${formatDate(new Date())} - ${certificateId}`,
+ popis: `Certifikát vygenerovaný ${formatDate(new Date())} - ${certificateId} (šablóna: ${templateName})`,
})
.returning();
@@ -419,6 +293,7 @@ export const generateCertificate = async (registraciaId) => {
certificateId,
participantName,
courseTitle: course.nazov,
+ templateUsed: templateName,
};
};
diff --git a/src/services/ai-kurzy/registracie.service.js b/src/services/ai-kurzy/registracie.service.js
index 5bc8a8f..e57485a 100644
--- a/src/services/ai-kurzy/registracie.service.js
+++ b/src/services/ai-kurzy/registracie.service.js
@@ -146,6 +146,7 @@ export const getCombinedTableData = async () => {
poznamka: registracie.poznamka,
createdAt: registracie.createdAt,
dokumentyCount: sql`(SELECT COUNT(*) FROM prilohy WHERE registracia_id = ${registracie.id})::int`,
+ hasCertificate: sql`(SELECT COUNT(*) > 0 FROM prilohy WHERE registracia_id = ${registracie.id} AND typ_prilohy = 'certifikat')::boolean`,
})
.from(registracie)
.innerJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id))
diff --git a/src/templates/certificates/AIcertifikat.html b/src/templates/certificates/AIcertifikat.html
new file mode 100644
index 0000000..412a5ce
--- /dev/null
+++ b/src/templates/certificates/AIcertifikat.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+ Osvedčenie - {{participantName}}
+
+
+
+
+
+
O S V E D Č E N I E
+
o absolvovaní kurzu
+
+
+
{{courseTitle}}
+ {{#if courseModules}}
+
{{courseModules}}
+ {{/if}}
+ {{#if dateRange}}
+
{{dateRange}}
+ {{/if}}
+
+
+
{{participantName}}
+
+
+
+
+
ID: {{certificateId}}
+
+
+