feat: Add user management APIs, status enum, enhanced notifications
- Add updateUser and resetUserPassword admin endpoints - Change company status from boolean to enum (registered, lead, customer, inactive) - Add 'important' event type to calendar validators and email templates - Add 1-hour-before event notifications cron job - Add 18:00 evening notifications for next-day events - Add contact description field support - Fix count() function usage in admin service - Add SQL migrations for schema changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,9 @@ const formatTime = (date) => {
|
||||
* @returns {string}
|
||||
*/
|
||||
const getTypeLabel = (type) => {
|
||||
return type === 'meeting' ? 'Stretnutie' : 'Udalosť';
|
||||
if (type === 'meeting') return 'Stretnutie';
|
||||
if (type === 'important') return 'Dôležité';
|
||||
return 'Udalosť';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,7 +45,9 @@ const getTypeLabel = (type) => {
|
||||
* @returns {string}
|
||||
*/
|
||||
const getTypeBadgeColor = (type) => {
|
||||
return type === 'meeting' ? '#22c55e' : '#3b82f6';
|
||||
if (type === 'meeting') return '#22c55e';
|
||||
if (type === 'important') return '#ef4444';
|
||||
return '#3b82f6';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,6 +66,23 @@ const getTomorrowRange = () => {
|
||||
return { startOfTomorrow, endOfTomorrow };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get range for events starting in the next hour
|
||||
* @returns {{ startOfRange: Date, endOfRange: Date }}
|
||||
*/
|
||||
const getOneHourRange = () => {
|
||||
const now = new Date();
|
||||
|
||||
// Start of range: now
|
||||
const startOfRange = new Date(now);
|
||||
|
||||
// End of range: 1 hour from now
|
||||
const endOfRange = new Date(now);
|
||||
endOfRange.setHours(endOfRange.getHours() + 1);
|
||||
|
||||
return { startOfRange, endOfRange };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get events starting tomorrow with assigned users
|
||||
* @returns {Promise<Array>} Events with user info
|
||||
@@ -102,6 +119,42 @@ const getTomorrowEvents = async () => {
|
||||
return tomorrowEvents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get events starting in the next hour with assigned users
|
||||
* @returns {Promise<Array>} Events with user info
|
||||
*/
|
||||
const getUpcomingEvents = async () => {
|
||||
const { startOfRange, endOfRange } = getOneHourRange();
|
||||
|
||||
logger.info(`Hľadám udalosti začínajúce v najbližšej hodine: ${startOfRange.toISOString()} - ${endOfRange.toISOString()}`);
|
||||
|
||||
// Get events starting in the next hour
|
||||
const upcomingEvents = 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(
|
||||
and(
|
||||
gte(events.start, startOfRange),
|
||||
lt(events.start, endOfRange)
|
||||
)
|
||||
);
|
||||
|
||||
return upcomingEvents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user's primary email address
|
||||
* @param {string} userId
|
||||
@@ -453,3 +506,98 @@ export const sendEventNotifications = async () => {
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send notifications for events starting in the next hour (1 hour before meeting)
|
||||
* @returns {Promise<{ sent: number, failed: number, skipped: number }>}
|
||||
*/
|
||||
export const sendOneHourBeforeNotifications = async () => {
|
||||
logger.info('=== Spúšťam kontrolu udalostí začínajúcich v najbližšej hodine ===');
|
||||
|
||||
const stats = { sent: 0, failed: 0, skipped: 0 };
|
||||
|
||||
// Get sender account
|
||||
const senderAccount = await getSenderAccount();
|
||||
if (!senderAccount) {
|
||||
logger.error('Nemôžem pokračovať bez platného odosielacieho účtu');
|
||||
return stats;
|
||||
}
|
||||
|
||||
const jmapConfig = {
|
||||
server: process.env.JMAP_SERVER,
|
||||
username: senderAccount.email,
|
||||
password: senderAccount.password,
|
||||
accountId: senderAccount.jmapAccountId,
|
||||
};
|
||||
|
||||
// Get events starting in the next hour
|
||||
const upcomingEvents = await getUpcomingEvents();
|
||||
|
||||
if (upcomingEvents.length === 0) {
|
||||
logger.info('Žiadne udalosti v najbližšej hodine');
|
||||
return stats;
|
||||
}
|
||||
|
||||
logger.info(`Nájdených ${upcomingEvents.length} priradení udalostí v najbližšej hodine`);
|
||||
|
||||
// Group events by user to avoid duplicate notifications for same event
|
||||
const userNotifications = new Map();
|
||||
|
||||
for (const row of upcomingEvents) {
|
||||
const key = `${row.userId}-${row.eventId}`;
|
||||
if (!userNotifications.has(key)) {
|
||||
userNotifications.set(key, {
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
firstName: row.firstName,
|
||||
lastName: row.lastName,
|
||||
event: {
|
||||
id: row.eventId,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
start: row.start,
|
||||
end: row.end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Unikátnych notifikácií na odoslanie: ${userNotifications.size}`);
|
||||
|
||||
// Send notifications
|
||||
for (const [key, data] of userNotifications) {
|
||||
const { userId, username, firstName, event } = data;
|
||||
|
||||
// Get user's email
|
||||
const userEmail = await getUserEmail(userId);
|
||||
|
||||
if (!userEmail) {
|
||||
stats.skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate email content with "1 hour before" subject
|
||||
const typeLabel = event.type === 'meeting' ? 'Stretnutie' : (event.type === 'important' ? 'Dôležité' : 'Udalosť');
|
||||
const subject = `Pripomienka: ${typeLabel} - ${event.title} (o 1 hodinu)`;
|
||||
const htmlBody = generateEventNotificationHtml({ firstName, username, event });
|
||||
const textBody = generateEventNotificationText({ firstName, username, event });
|
||||
|
||||
// Send email
|
||||
logger.info(`Odosielam 1h notifikáciu pre ${username} - udalosť: ${event.title}`);
|
||||
|
||||
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 (1h notifikácie): odoslaných ${stats.sent}, neúspešných ${stats.failed}, preskočených ${stats.skipped} ===`);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import cron from 'node-cron';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { sendEventNotifications, sendSingleEventNotification } from './event-notifier.js';
|
||||
import { sendEventNotifications, sendSingleEventNotification, sendOneHourBeforeNotifications } from './event-notifier.js';
|
||||
|
||||
/**
|
||||
* Parse NOTIFICATION_TIME from env (format: "HH:mm")
|
||||
@@ -58,6 +58,54 @@ export const startCalendarNotificationCron = () => {
|
||||
return { name: `Calendar (${schedule})`, schedule };
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the 1-hour-before notification cron job (runs every hour)
|
||||
* @returns {{ name: string, schedule: string }}
|
||||
*/
|
||||
export const startOneHourBeforeNotificationCron = () => {
|
||||
const cronExpression = '0 * * * *'; // Every hour at minute 0
|
||||
const schedule = 'every hour';
|
||||
|
||||
cron.schedule(cronExpression, async () => {
|
||||
logger.info('Running 1-hour-before notifications...');
|
||||
try {
|
||||
const stats = await sendOneHourBeforeNotifications();
|
||||
logger.info(`1h notifications done: sent=${stats.sent}, failed=${stats.failed}, skipped=${stats.skipped}`);
|
||||
} catch (error) {
|
||||
logger.error('1-hour-before notification cron failed', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: true,
|
||||
timezone: 'Europe/Bratislava',
|
||||
});
|
||||
|
||||
return { name: `1h Before (${schedule})`, schedule };
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the 18:00 notification cron job (tomorrow's events at 18:00)
|
||||
* @returns {{ name: string, schedule: string }}
|
||||
*/
|
||||
export const startEveningNotificationCron = () => {
|
||||
const cronExpression = '0 18 * * *'; // Every day at 18:00
|
||||
const schedule = '18:00';
|
||||
|
||||
cron.schedule(cronExpression, async () => {
|
||||
logger.info('Running evening notifications for tomorrow...');
|
||||
try {
|
||||
const stats = await sendEventNotifications();
|
||||
logger.info(`Evening notifications done: sent=${stats.sent}, failed=${stats.failed}, skipped=${stats.skipped}`);
|
||||
} catch (error) {
|
||||
logger.error('Evening notification cron failed', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: true,
|
||||
timezone: 'Europe/Bratislava',
|
||||
});
|
||||
|
||||
return { name: `Evening (${schedule})`, schedule };
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually trigger event notifications (for testing)
|
||||
*/
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { startCalendarNotificationCron, triggerEventNotifications, triggerSingleEventNotification } from './calendar/index.js';
|
||||
import { startCalendarNotificationCron, startOneHourBeforeNotificationCron, startEveningNotificationCron, triggerEventNotifications, triggerSingleEventNotification } from './calendar/index.js';
|
||||
import { startAuditCleanupCron, cleanupOldAuditLogs } from './cleanupAuditLogs.js';
|
||||
|
||||
/**
|
||||
* Start all cron jobs
|
||||
*/
|
||||
export const startAllCronJobs = () => {
|
||||
// Calendar event notifications
|
||||
// Calendar event notifications (morning - configurable time)
|
||||
const calendarJob = startCalendarNotificationCron();
|
||||
logger.info(`Cron: ${calendarJob.name}`);
|
||||
|
||||
// 1-hour-before notifications (every hour)
|
||||
const oneHourJob = startOneHourBeforeNotificationCron();
|
||||
logger.info(`Cron: ${oneHourJob.name}`);
|
||||
|
||||
// Evening notifications at 18:00 for tomorrow's events
|
||||
const eveningJob = startEveningNotificationCron();
|
||||
logger.info(`Cron: ${eveningJob.name}`);
|
||||
|
||||
// Audit logs cleanup
|
||||
const auditJob = startAuditCleanupCron();
|
||||
logger.info(`Cron: ${auditJob.name}`);
|
||||
|
||||
Reference in New Issue
Block a user