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}}
+
+ +