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:
richardtekula
2026-01-15 09:41:29 +01:00
parent 5d01fc9542
commit 70fa080455
13 changed files with 423 additions and 19 deletions

View File

@@ -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';
};
/**

View File

@@ -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;
};

View File

@@ -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)
*/

View File

@@ -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}`);