diff --git a/package-lock.json b/package-lock.json
index 0d78bf7..2f748bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"multer": "^2.0.2",
+ "node-cron": "^4.2.1",
"pg": "^8.16.3",
"uuid": "^13.0.0",
"xss-clean": "^0.1.4",
@@ -6431,6 +6432,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-cron": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+ "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
diff --git a/package.json b/package.json
index c27bef8..04296ff 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"multer": "^2.0.2",
+ "node-cron": "^4.2.1",
"pg": "^8.16.3",
"uuid": "^13.0.0",
"xss-clean": "^0.1.4",
diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js
index 7cb01df..311bfdd 100644
--- a/src/controllers/admin.controller.js
+++ b/src/controllers/admin.controller.js
@@ -1,6 +1,7 @@
import * as adminService from '../services/admin.service.js';
import * as statusService from '../services/status.service.js';
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
+import { triggerEventNotifications } from '../cron/index.js';
/**
* Vytvorenie nového usera s automatic temporary password (admin only)
@@ -160,3 +161,21 @@ export const getServerStatus = async (req, res, next) => {
next(error);
}
};
+
+/**
+ * Manually trigger event notifications (admin only, for testing)
+ * POST /api/admin/trigger-notifications
+ */
+export const triggerNotifications = async (req, res, next) => {
+ try {
+ const stats = await triggerEventNotifications();
+
+ res.status(200).json({
+ success: true,
+ data: stats,
+ message: `Notifikácie odoslané: ${stats.sent}, neúspešné: ${stats.failed}, preskočené: ${stats.skipped}`,
+ });
+ } catch (error) {
+ next(error);
+ }
+};
diff --git a/src/cron/calendar/email-template.js b/src/cron/calendar/email-template.js
new file mode 100644
index 0000000..773787c
--- /dev/null
+++ b/src/cron/calendar/email-template.js
@@ -0,0 +1,229 @@
+/**
+ * HTML Email Template for Event Notifications
+ */
+
+/**
+ * Format date to Slovak locale
+ * @param {Date} date
+ * @returns {string}
+ */
+const formatDate = (date) => {
+ return new Intl.DateTimeFormat('sk-SK', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ }).format(new Date(date));
+};
+
+/**
+ * Format time to Slovak locale
+ * @param {Date} date
+ * @returns {string}
+ */
+const formatTime = (date) => {
+ return new Intl.DateTimeFormat('sk-SK', {
+ hour: '2-digit',
+ minute: '2-digit',
+ }).format(new Date(date));
+};
+
+/**
+ * Get event type label in Slovak
+ * @param {string} type
+ * @returns {string}
+ */
+const getTypeLabel = (type) => {
+ return type === 'meeting' ? 'Stretnutie' : 'Udalosť';
+};
+
+/**
+ * Get badge color based on event type
+ * @param {string} type
+ * @returns {string}
+ */
+const getTypeBadgeColor = (type) => {
+ return type === 'meeting' ? '#22c55e' : '#3b82f6';
+};
+
+/**
+ * Generate HTML email template for event notification
+ * @param {Object} params
+ * @param {string} params.firstName - User's first name
+ * @param {string} params.username - User's username (fallback)
+ * @param {Object} params.event - Event object
+ * @param {string} params.event.title - Event title
+ * @param {string} params.event.description - Event description
+ * @param {string} params.event.type - Event type ('meeting' or 'event')
+ * @param {Date} params.event.start - Event start time
+ * @param {Date} params.event.end - Event end time
+ * @returns {string} HTML email content
+ */
+export const generateEventNotificationHtml = ({ firstName, username, event }) => {
+ const displayName = firstName || username || 'Používateľ';
+ const typeLabel = getTypeLabel(event.type);
+ const badgeColor = getTypeBadgeColor(event.type);
+
+ return `
+
+
+
+
+
+ Pripomienka udalosti - CRM
+
+
+
+
+
+
+
+
+
+ |
+
+ CRM
+
+
+ Pripomienka udalosti
+
+ |
+
+
+
+
+ |
+
+ Ahoj ${displayName},
+
+
+
+ Zajtra máš naplánovanú udalosť v kalendári:
+
+
+
+
+
+ |
+
+
+
+ ${typeLabel}
+
+
+
+
+
+ ${event.title}
+
+
+ ${event.description ? `
+
+
+ ${event.description}
+
+ ` : ''}
+
+
+
+
+
+
+
+ |
+ 📅
+ |
+
+
+ ${formatDate(event.start)}
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+ 🕐
+ |
+
+
+ ${formatTime(event.start)} - ${formatTime(event.end)}
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+ Prajeme ti produktívny deň!
+
+ |
+
+
+
+
+ |
+
+ Táto správa bola automaticky vygenerovaná systémom CRM.
+ Prosím, neodpovedajte na tento email.
+
+ |
+
+
+
+ |
+
+
+
+
+`.trim();
+};
+
+/**
+ * Generate plain text version of the email
+ * @param {Object} params
+ * @param {string} params.firstName - User's first name
+ * @param {string} params.username - User's username (fallback)
+ * @param {Object} params.event - Event object
+ * @returns {string} Plain text email content
+ */
+export const generateEventNotificationText = ({ firstName, username, event }) => {
+ const displayName = firstName || username || 'Používateľ';
+ const typeLabel = getTypeLabel(event.type);
+
+ return `
+Ahoj ${displayName},
+
+Zajtra máš naplánovanú udalosť v kalendári:
+
+${typeLabel.toUpperCase()}: ${event.title}
+${event.description ? `Popis: ${event.description}\n` : ''}
+Dátum: ${formatDate(event.start)}
+Čas: ${formatTime(event.start)} - ${formatTime(event.end)}
+
+Prajeme ti produktívny deň!
+
+---
+Táto správa bola automaticky vygenerovaná systémom CRM.
+Prosím, neodpovedajte na tento email.
+`.trim();
+};
+
+/**
+ * Generate email subject
+ * @param {Object} event
+ * @returns {string}
+ */
+export const generateEventNotificationSubject = (event) => {
+ const typeLabel = getTypeLabel(event.type);
+ return `Pripomienka: ${typeLabel} - ${event.title} (zajtra)`;
+};
diff --git a/src/cron/calendar/event-notifier.js b/src/cron/calendar/event-notifier.js
new file mode 100644
index 0000000..5aea55b
--- /dev/null
+++ b/src/cron/calendar/event-notifier.js
@@ -0,0 +1,318 @@
+import { eq, and, gte, lt } from 'drizzle-orm';
+import { db } from '../../config/database.js';
+import { events, eventUsers, users, emailAccounts, userEmailAccounts } from '../../db/schema.js';
+import { decryptPassword } from '../../utils/password.js';
+import { jmapRequest, getMailboxes, getIdentities } from '../../services/jmap/client.js';
+import { logger } from '../../utils/logger.js';
+import {
+ generateEventNotificationHtml,
+ generateEventNotificationText,
+ generateEventNotificationSubject,
+} from './email-template.js';
+
+/**
+ * Get sender email account credentials from database
+ * @returns {Promise