From 70fa080455ee6430517663cfb4bdea364cee099a Mon Sep 17 00:00:00 2001 From: richardtekula Date: Thu, 15 Jan 2026 09:41:29 +0100 Subject: [PATCH] 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 --- src/controllers/admin.controller.js | 41 +++++++ src/cron/calendar/email-template.js | 8 +- src/cron/calendar/event-notifier.js | 148 +++++++++++++++++++++++ src/cron/calendar/index.js | 50 +++++++- src/cron/index.js | 12 +- src/db/migrations/fix_status_type.sql | 24 ++++ src/db/migrations/hotfix_migration.sql | 38 ++++++ src/db/schema.js | 4 +- src/routes/admin.routes.js | 18 +++ src/services/admin.service.js | 78 +++++++++++- src/services/company.service.js | 11 +- src/services/personal-contact.service.js | 3 + src/validators/crm.validators.js | 7 +- 13 files changed, 423 insertions(+), 19 deletions(-) create mode 100644 src/db/migrations/fix_status_type.sql create mode 100644 src/db/migrations/hotfix_migration.sql diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js index a5d2167..736c2ea 100644 --- a/src/controllers/admin.controller.js +++ b/src/controllers/admin.controller.js @@ -125,6 +125,47 @@ export const changeUserRole = async (req, res, next) => { } }; +/** + * Update user details (admin only) + * PATCH /api/admin/users/:userId + */ +export const updateUser = async (req, res, next) => { + const { userId } = req.params; + const { firstName, lastName } = req.body; + + try { + const updated = await adminService.updateUser(userId, { firstName, lastName }); + + res.status(200).json({ + success: true, + data: updated, + message: 'Používateľ bol aktualizovaný', + }); + } catch (error) { + next(error); + } +}; + +/** + * Reset user password (admin only) + * POST /api/admin/users/:userId/reset-password + */ +export const resetUserPassword = async (req, res, next) => { + const { userId } = req.params; + + try { + const result = await adminService.resetUserPassword(userId); + + res.status(200).json({ + success: true, + data: { tempPassword: result.tempPassword }, + message: 'Heslo bolo resetované', + }); + } catch (error) { + next(error); + } +}; + /** * Zmazanie usera (admin only) * DELETE /api/admin/users/:userId diff --git a/src/cron/calendar/email-template.js b/src/cron/calendar/email-template.js index 773787c..a58425c 100644 --- a/src/cron/calendar/email-template.js +++ b/src/cron/calendar/email-template.js @@ -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'; }; /** diff --git a/src/cron/calendar/event-notifier.js b/src/cron/calendar/event-notifier.js index 691c7ac..e22d1fe 100644 --- a/src/cron/calendar/event-notifier.js +++ b/src/cron/calendar/event-notifier.js @@ -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} 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} 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; +}; diff --git a/src/cron/calendar/index.js b/src/cron/calendar/index.js index db4c09b..bc87b70 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, 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) */ diff --git a/src/cron/index.js b/src/cron/index.js index f632e34..e9394a3 100644 --- a/src/cron/index.js +++ b/src/cron/index.js @@ -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}`); diff --git a/src/db/migrations/fix_status_type.sql b/src/db/migrations/fix_status_type.sql new file mode 100644 index 0000000..d756369 --- /dev/null +++ b/src/db/migrations/fix_status_type.sql @@ -0,0 +1,24 @@ +-- Fix: Convert status column from boolean to company_status enum +-- The previous migration renamed is_active to status but kept the boolean type + +-- Step 1: Add a temporary column with the correct enum type +ALTER TABLE "companies" ADD COLUMN "status_new" company_status; + +-- Step 2: Migrate data - convert boolean to enum +-- true (active) -> 'registered', false (inactive) -> 'inactive' +UPDATE "companies" SET "status_new" = + CASE + WHEN "status"::text = 'true' OR "status"::text = 't' THEN 'registered'::company_status + WHEN "status"::text = 'false' OR "status"::text = 'f' THEN 'inactive'::company_status + ELSE 'registered'::company_status + END; + +-- Step 3: Drop the old column +ALTER TABLE "companies" DROP COLUMN "status"; + +-- Step 4: Rename the new column +ALTER TABLE "companies" RENAME COLUMN "status_new" TO "status"; + +-- Step 5: Set the default and not null constraint +ALTER TABLE "companies" ALTER COLUMN "status" SET DEFAULT 'registered'; +ALTER TABLE "companies" ALTER COLUMN "status" SET NOT NULL; diff --git a/src/db/migrations/hotfix_migration.sql b/src/db/migrations/hotfix_migration.sql new file mode 100644 index 0000000..0ead94c --- /dev/null +++ b/src/db/migrations/hotfix_migration.sql @@ -0,0 +1,38 @@ +-- Hotfix Migration Script +-- Run this manually after accepting drizzle-kit push prompts + +-- 1. Create the company_status enum type if it doesn't exist +DO $$ BEGIN + CREATE TYPE company_status AS ENUM ('registered', 'lead', 'customer', 'inactive'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- 2. Add the status column to companies table if it doesn't exist +DO $$ BEGIN + ALTER TABLE companies ADD COLUMN status company_status NOT NULL DEFAULT 'registered'; +EXCEPTION + WHEN duplicate_column THEN null; +END $$; + +-- 3. Migrate data from is_active to status (if is_active column exists) +DO $$ BEGIN + UPDATE companies SET status = CASE + WHEN is_active = true THEN 'customer'::company_status + ELSE 'inactive'::company_status + END; +EXCEPTION + WHEN undefined_column THEN null; +END $$; + +-- 4. Drop the is_active column (optional - run after verifying migration) +-- ALTER TABLE companies DROP COLUMN IF EXISTS is_active; + +-- 5. Add description column to personal_contacts if it doesn't exist +DO $$ BEGIN + ALTER TABLE personal_contacts ADD COLUMN description TEXT; +EXCEPTION + WHEN duplicate_column THEN null; +END $$; + +-- Note: Event type 'important' is stored as text, no schema change needed for events table diff --git a/src/db/schema.js b/src/db/schema.js index edf200a..2c9999c 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -5,6 +5,7 @@ export const roleEnum = pgEnum('role', ['admin', 'member']); export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']); export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']); export const todoPriorityEnum = pgEnum('todo_priority', ['low', 'medium', 'high', 'urgent']); +export const companyStatusEnum = pgEnum('company_status', ['registered', 'lead', 'customer', 'inactive']); // Users table - používatelia systému export const users = pgTable('users', { @@ -89,6 +90,7 @@ export const personalContacts = pgTable('personal_contacts', { phone: text('phone').notNull(), email: text('email').notNull(), secondaryEmail: text('secondary_email'), + description: text('description'), // popis kontaktu createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => ({ @@ -128,7 +130,7 @@ export const companies = pgTable('companies', { phone: text('phone'), email: text('email'), website: text('website'), - isActive: boolean('is_active').default(true).notNull(), // či je firma aktívna + status: companyStatusEnum('status').default('registered').notNull(), // stav firmy createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index c9079a5..321b4aa 100644 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -31,6 +31,17 @@ router.get( adminController.getUser ); +// Update user details +router.patch( + '/users/:userId', + validateParams(z.object({ userId: z.string().uuid() })), + validateBody(z.object({ + firstName: z.string().max(100).optional(), + lastName: z.string().max(100).optional(), + })), + adminController.updateUser +); + // Zmena role usera router.patch( '/users/:userId/role', @@ -39,6 +50,13 @@ router.patch( adminController.changeUserRole ); +// Reset user password +router.post( + '/users/:userId/reset-password', + validateParams(z.object({ userId: z.string().uuid() })), + adminController.resetUserPassword +); + // Zmazanie usera router.delete( '/users/:userId', diff --git a/src/services/admin.service.js b/src/services/admin.service.js index 60f4422..1a55c3c 100644 --- a/src/services/admin.service.js +++ b/src/services/admin.service.js @@ -1,6 +1,6 @@ import { db } from '../config/database.js'; import { users, userEmailAccounts, emailAccounts } from '../db/schema.js'; -import { eq } from 'drizzle-orm'; +import { eq, count } from 'drizzle-orm'; import { hashPassword, generateTempPassword } from '../utils/password.js'; import { ConflictError, NotFoundError } from '../utils/errors.js'; import * as emailAccountService from './email-account.service.js'; @@ -162,6 +162,74 @@ export const changeUserRole = async (userId, newRole) => { return { userId, oldRole, newRole }; }; +/** + * Update user details (firstName, lastName) + */ +export const updateUser = async (userId, data) => { + const [user] = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (!user) { + throw new NotFoundError('Používateľ nenájdený'); + } + + const { firstName, lastName } = data; + + const [updated] = await db + .update(users) + .set({ + firstName: firstName !== undefined ? firstName : user.firstName, + lastName: lastName !== undefined ? lastName : user.lastName, + updatedAt: new Date(), + }) + .where(eq(users.id, userId)) + .returning({ + id: users.id, + username: users.username, + firstName: users.firstName, + lastName: users.lastName, + role: users.role, + changedPassword: users.changedPassword, + lastLogin: users.lastLogin, + createdAt: users.createdAt, + }); + + return updated; +}; + +/** + * Reset user password (generate new temp password) + */ +export const resetUserPassword = async (userId) => { + const [user] = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (!user) { + throw new NotFoundError('Používateľ nenájdený'); + } + + const tempPassword = generateTempPassword(12); + const hashedTempPassword = await hashPassword(tempPassword); + + await db + .update(users) + .set({ + tempPassword: hashedTempPassword, + password: null, + changedPassword: false, + updatedAt: new Date(), + }) + .where(eq(users.id, userId)); + + return { userId, tempPassword }; +}; + /** * Zmazanie usera */ @@ -179,11 +247,11 @@ export const deleteUser = async (userId) => { // Zabraň zmazaniu posledného admina if (user.role === 'admin') { const [adminCount] = await db - .select({ count: db.$count(users) }) + .select({ count: count() }) .from(users) .where(eq(users.role, 'admin')); - if (adminCount.count <= 1) { + if ((adminCount?.count || 0) <= 1) { throw new ConflictError('Nemôžete zmazať posledného administrátora'); } } @@ -203,11 +271,11 @@ export const deleteUser = async (userId) => { let deletedEmailAccounts = 0; for (const emailAccountId of emailAccountIds) { const [remainingLinks] = await db - .select({ count: db.$count(userEmailAccounts) }) + .select({ count: count() }) .from(userEmailAccounts) .where(eq(userEmailAccounts.emailAccountId, emailAccountId)); - if (remainingLinks.count === 0) { + if ((remainingLinks?.count || 0) === 0) { await db.delete(emailAccounts).where(eq(emailAccounts.id, emailAccountId)); deletedEmailAccounts++; } diff --git a/src/services/company.service.js b/src/services/company.service.js index c1317f9..7368907 100644 --- a/src/services/company.service.js +++ b/src/services/company.service.js @@ -32,7 +32,7 @@ export const getAllCompanies = async (searchTerm = null, userId = null, userRole phone: companies.phone, email: companies.email, website: companies.website, - isActive: companies.isActive, + status: companies.status, createdBy: companies.createdBy, createdAt: companies.createdAt, updatedAt: companies.updatedAt, @@ -85,7 +85,7 @@ export const getCompanyById = async (companyId) => { phone: companies.phone, email: companies.email, website: companies.website, - isActive: companies.isActive, + status: companies.status, createdBy: companies.createdBy, createdAt: companies.createdAt, updatedAt: companies.updatedAt, @@ -110,7 +110,7 @@ export const getCompanyById = async (companyId) => { * Create new company */ export const createCompany = async (userId, data) => { - const { name, description, address, city, country, phone, email, website } = data; + const { name, description, address, city, country, phone, email, website, status } = data; // Check if company with same name already exists const [existing] = await db @@ -134,6 +134,7 @@ export const createCompany = async (userId, data) => { phone: phone || null, email: email || null, website: website || null, + status: status || 'registered', createdBy: userId, }) .returning(); @@ -147,7 +148,7 @@ export const createCompany = async (userId, data) => { export const updateCompany = async (companyId, data) => { const company = await getCompanyById(companyId); - const { name, description, address, city, country, phone, email, website, isActive } = data; + const { name, description, address, city, country, phone, email, website, status } = data; // If name is being changed, check for duplicates if (name && name !== company.name) { @@ -173,7 +174,7 @@ export const updateCompany = async (companyId, data) => { phone: phone !== undefined ? phone : company.phone, email: email !== undefined ? email : company.email, website: website !== undefined ? website : company.website, - isActive: isActive !== undefined ? isActive : company.isActive, + status: status !== undefined ? status : company.status, updatedAt: new Date(), }) .where(eq(companies.id, companyId)) diff --git a/src/services/personal-contact.service.js b/src/services/personal-contact.service.js index 56f9c84..6897dde 100644 --- a/src/services/personal-contact.service.js +++ b/src/services/personal-contact.service.js @@ -14,6 +14,7 @@ export const listPersonalContacts = async (userId) => { phone: personalContacts.phone, email: personalContacts.email, secondaryEmail: personalContacts.secondaryEmail, + description: personalContacts.description, createdAt: personalContacts.createdAt, updatedAt: personalContacts.updatedAt, companyName: companies.name, @@ -69,6 +70,7 @@ export const createPersonalContact = async (userId, contact) => { phone: contact.phone, email: contact.email, secondaryEmail: contact.secondaryEmail || null, + description: contact.description || null, }) .returning() @@ -104,6 +106,7 @@ export const updatePersonalContact = async (contactId, userId, updates) => { if (updates.email !== undefined) updateData.email = updates.email if (updates.secondaryEmail !== undefined) updateData.secondaryEmail = updates.secondaryEmail if (updates.companyId !== undefined) updateData.companyId = updates.companyId || null + if (updates.description !== undefined) updateData.description = updates.description || null const [updated] = await db .update(personalContacts) diff --git a/src/validators/crm.validators.js b/src/validators/crm.validators.js index 1bda5b8..35cc101 100644 --- a/src/validators/crm.validators.js +++ b/src/validators/crm.validators.js @@ -15,6 +15,7 @@ export const createCompanySchema = z.object({ phone: z.string().max(50).optional(), email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')), website: z.string().url('Neplatný formát URL').max(255).optional().or(z.literal('')), + status: z.enum(['registered', 'lead', 'customer', 'inactive']).optional(), }); export const updateCompanySchema = z.object({ @@ -26,7 +27,7 @@ export const updateCompanySchema = z.object({ phone: z.string().max(50).optional(), email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')), website: z.string().url('Neplatný formát URL').max(255).optional().or(z.literal('')), - isActive: z.boolean().optional(), + status: z.enum(['registered', 'lead', 'customer', 'inactive']).optional(), }); // Project validators @@ -176,7 +177,7 @@ export const updateTimeEntrySchema = z.object({ export const createEventSchema = z.object({ title: z.string().min(1, 'Názov je povinný'), description: z.string().optional(), - type: z.enum(['meeting', 'event']).default('meeting'), + type: z.enum(['meeting', 'event', 'important']).default('meeting'), start: z.string().min(1, 'Začiatok je povinný'), end: z.string().min(1, 'Koniec je povinný'), assignedUserIds: z.array(z.string().uuid('Neplatný formát user ID')).optional(), @@ -185,7 +186,7 @@ export const createEventSchema = z.object({ export const updateEventSchema = z.object({ title: z.string().min(1).optional(), description: z.string().optional(), - type: z.enum(['meeting', 'event']).optional(), + type: z.enum(['meeting', 'event', 'important']).optional(), start: z.string().optional(), end: z.string().optional(), assignedUserIds: z.array(z.string().uuid('Neplatný formát user ID')).optional(),