diff --git a/src/controllers/event.controller.js b/src/controllers/event.controller.js index 8404206..7bb4701 100644 --- a/src/controllers/event.controller.js +++ b/src/controllers/event.controller.js @@ -1,4 +1,5 @@ import * as eventService from '../services/event.service.js'; +import { triggerSingleEventNotification } from '../cron/index.js'; /** * Get calendar data (events + todos) by month @@ -106,3 +107,24 @@ export const deleteEvent = async (req, res, next) => { next(error); } }; + +/** + * Send notification for an event to all assigned users (admin only) + * POST /api/events/:eventId/notify + */ +export const sendEventNotification = async (req, res, next) => { + try { + const { eventId } = req.params; + const adminUserId = req.userId; + + const stats = await triggerSingleEventNotification(eventId, adminUserId); + + res.status(200).json({ + success: true, + data: stats, + message: `Notifikácie pre "${stats.eventTitle}" odoslané: ${stats.sent}, neúspešné: ${stats.failed}, preskočené: ${stats.skipped}`, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/cron/calendar/event-notifier.js b/src/cron/calendar/event-notifier.js index 5aea55b..171506f 100644 --- a/src/cron/calendar/event-notifier.js +++ b/src/cron/calendar/event-notifier.js @@ -222,6 +222,145 @@ const sendNotificationEmail = async (jmapConfig, to, subject, htmlBody, textBody } }; +/** + * Get sender account by user ID (admin's primary email) + * @param {string} userId - Admin user ID + * @returns {Promise} Sender account with decrypted password + */ +const getSenderAccountByUserId = async (userId) => { + const [result] = await db + .select({ + id: emailAccounts.id, + email: emailAccounts.email, + emailPassword: emailAccounts.emailPassword, + jmapAccountId: emailAccounts.jmapAccountId, + }) + .from(userEmailAccounts) + .innerJoin(emailAccounts, eq(userEmailAccounts.emailAccountId, emailAccounts.id)) + .where( + and( + eq(userEmailAccounts.userId, userId), + eq(userEmailAccounts.isPrimary, true) + ) + ) + .limit(1); + + if (!result) { + logger.error(`Používateľ ${userId} nemá nastavený primárny email účet`); + return null; + } + + try { + const decryptedPassword = decryptPassword(result.emailPassword); + return { + id: result.id, + email: result.email, + password: decryptedPassword, + jmapAccountId: result.jmapAccountId, + }; + } catch (error) { + logger.error(`Nepodarilo sa dešifrovať heslo pre ${result.email}`, error); + return null; + } +}; + +/** + * Send notification for a single event to all assigned users + * @param {string} eventId - Event ID + * @param {string} adminUserId - Admin user ID (sender) + * @returns {Promise<{ sent: number, failed: number, skipped: number, eventTitle: string }>} + */ +export const sendSingleEventNotification = async (eventId, adminUserId) => { + logger.info(`=== Odosielam notifikácie pre event ${eventId} od admina ${adminUserId} ===`); + + const stats = { sent: 0, failed: 0, skipped: 0, eventTitle: '' }; + + // Get admin's primary email account + const senderAccount = await getSenderAccountByUserId(adminUserId); + if (!senderAccount) { + throw new Error('Nemáte nastavený primárny email účet. Nastavte ho v profile.'); + } + + const jmapConfig = { + server: process.env.JMAP_SERVER, + username: senderAccount.email, + password: senderAccount.password, + accountId: senderAccount.jmapAccountId, + }; + + logger.info(`Odosielam z účtu: ${senderAccount.email}`); + + // Get the event with assigned users + const eventData = await db + .select({ + eventId: events.id, + title: events.title, + description: events.description, + type: events.type, + start: events.start, + end: events.end, + userId: users.id, + username: users.username, + firstName: users.firstName, + lastName: users.lastName, + }) + .from(events) + .innerJoin(eventUsers, eq(events.id, eventUsers.eventId)) + .innerJoin(users, eq(eventUsers.userId, users.id)) + .where(eq(events.id, eventId)); + + if (eventData.length === 0) { + throw new Error('Event nebol nájdený alebo nemá priradených používateľov'); + } + + stats.eventTitle = eventData[0].title; + logger.info(`Event: ${stats.eventTitle}, priradených používateľov: ${eventData.length}`); + + // Send notifications to each assigned user + for (const row of eventData) { + const { userId, username, firstName } = row; + const event = { + id: row.eventId, + title: row.title, + description: row.description, + type: row.type, + start: row.start, + end: row.end, + }; + + // Get user's email + const userEmail = await getUserEmail(userId); + + if (!userEmail) { + logger.warn(`Používateľ ${username} nemá nastavený primárny email - preskakujem`); + stats.skipped++; + continue; + } + + // Generate email content + const subject = generateEventNotificationSubject(event); + const htmlBody = generateEventNotificationHtml({ firstName, username, event }); + const textBody = generateEventNotificationText({ firstName, username, event }); + + // Send email + logger.info(`Odosielam notifikáciu pre ${username} (${userEmail})`); + + const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody); + + if (success) { + logger.success(`Email úspešne odoslaný na ${userEmail}`); + stats.sent++; + } else { + logger.error(`Nepodarilo sa odoslať email na ${userEmail}`); + stats.failed++; + } + } + + logger.info(`=== Hotovo: odoslaných ${stats.sent}, neúspešných ${stats.failed}, preskočených ${stats.skipped} ===`); + + return stats; +}; + /** * Main function to send event notifications * @returns {Promise<{ sent: number, failed: number, skipped: number }>} diff --git a/src/cron/calendar/index.js b/src/cron/calendar/index.js index c539a64..197bfa9 100644 --- a/src/cron/calendar/index.js +++ b/src/cron/calendar/index.js @@ -1,6 +1,6 @@ import cron from 'node-cron'; import { logger } from '../../utils/logger.js'; -import { sendEventNotifications } from './event-notifier.js'; +import { sendEventNotifications, sendSingleEventNotification } from './event-notifier.js'; /** * Parse NOTIFICATION_TIME from env (format: "HH:mm") @@ -71,3 +71,13 @@ export const triggerEventNotifications = async () => { logger.info('Manuálne spúšťam kontrolu notifikácií...'); return sendEventNotifications(); }; + +/** + * Send notification for a single event + * @param {string} eventId - Event ID + * @param {string} adminUserId - Admin user ID (sender) + */ +export const triggerSingleEventNotification = async (eventId, adminUserId) => { + logger.info(`Manuálne spúšťam notifikáciu pre event ${eventId} od admina ${adminUserId}...`); + return sendSingleEventNotification(eventId, adminUserId); +}; diff --git a/src/cron/index.js b/src/cron/index.js index ff4585e..7b9a0cf 100644 --- a/src/cron/index.js +++ b/src/cron/index.js @@ -1,5 +1,5 @@ import { logger } from '../utils/logger.js'; -import { startCalendarNotificationCron, triggerEventNotifications } from './calendar/index.js'; +import { startCalendarNotificationCron, triggerEventNotifications, triggerSingleEventNotification } from './calendar/index.js'; /** * Start all cron jobs @@ -14,4 +14,4 @@ export const startAllCronJobs = () => { }; // Export individual functions for testing/manual triggers -export { triggerEventNotifications }; +export { triggerEventNotifications, triggerSingleEventNotification }; diff --git a/src/routes/event.routes.js b/src/routes/event.routes.js index de5e8a9..f9805a7 100644 --- a/src/routes/event.routes.js +++ b/src/routes/event.routes.js @@ -69,4 +69,14 @@ router.delete( eventController.deleteEvent ); +/** + * POST /api/events/:eventId/notify - Odoslať notifikácie priradeným používateľom (iba admin) + */ +router.post( + '/:eventId/notify', + requireAdmin, + validateParams(eventIdSchema), + eventController.sendEventNotification +); + export default router;