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

@@ -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) * Zmazanie usera (admin only)
* DELETE /api/admin/users/:userId * DELETE /api/admin/users/:userId

View File

@@ -34,7 +34,9 @@ const formatTime = (date) => {
* @returns {string} * @returns {string}
*/ */
const getTypeLabel = (type) => { 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} * @returns {string}
*/ */
const getTypeBadgeColor = (type) => { 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 }; 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 * Get events starting tomorrow with assigned users
* @returns {Promise<Array>} Events with user info * @returns {Promise<Array>} Events with user info
@@ -102,6 +119,42 @@ const getTomorrowEvents = async () => {
return tomorrowEvents; 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 * Get user's primary email address
* @param {string} userId * @param {string} userId
@@ -453,3 +506,98 @@ export const sendEventNotifications = async () => {
return stats; 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 cron from 'node-cron';
import { logger } from '../../utils/logger.js'; 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") * Parse NOTIFICATION_TIME from env (format: "HH:mm")
@@ -58,6 +58,54 @@ export const startCalendarNotificationCron = () => {
return { name: `Calendar (${schedule})`, schedule }; 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) * Manually trigger event notifications (for testing)
*/ */

View File

@@ -1,15 +1,23 @@
import { logger } from '../utils/logger.js'; 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'; import { startAuditCleanupCron, cleanupOldAuditLogs } from './cleanupAuditLogs.js';
/** /**
* Start all cron jobs * Start all cron jobs
*/ */
export const startAllCronJobs = () => { export const startAllCronJobs = () => {
// Calendar event notifications // Calendar event notifications (morning - configurable time)
const calendarJob = startCalendarNotificationCron(); const calendarJob = startCalendarNotificationCron();
logger.info(`Cron: ${calendarJob.name}`); 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 // Audit logs cleanup
const auditJob = startAuditCleanupCron(); const auditJob = startAuditCleanupCron();
logger.info(`Cron: ${auditJob.name}`); logger.info(`Cron: ${auditJob.name}`);

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ export const roleEnum = pgEnum('role', ['admin', 'member']);
export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']); export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']);
export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']); export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']);
export const todoPriorityEnum = pgEnum('todo_priority', ['low', 'medium', 'high', 'urgent']); 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 // Users table - používatelia systému
export const users = pgTable('users', { export const users = pgTable('users', {
@@ -89,6 +90,7 @@ export const personalContacts = pgTable('personal_contacts', {
phone: text('phone').notNull(), phone: text('phone').notNull(),
email: text('email').notNull(), email: text('email').notNull(),
secondaryEmail: text('secondary_email'), secondaryEmail: text('secondary_email'),
description: text('description'), // popis kontaktu
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({ }, (table) => ({
@@ -128,7 +130,7 @@ export const companies = pgTable('companies', {
phone: text('phone'), phone: text('phone'),
email: text('email'), email: text('email'),
website: text('website'), 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' }), createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),

View File

@@ -31,6 +31,17 @@ router.get(
adminController.getUser 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 // Zmena role usera
router.patch( router.patch(
'/users/:userId/role', '/users/:userId/role',
@@ -39,6 +50,13 @@ router.patch(
adminController.changeUserRole adminController.changeUserRole
); );
// Reset user password
router.post(
'/users/:userId/reset-password',
validateParams(z.object({ userId: z.string().uuid() })),
adminController.resetUserPassword
);
// Zmazanie usera // Zmazanie usera
router.delete( router.delete(
'/users/:userId', '/users/:userId',

View File

@@ -1,6 +1,6 @@
import { db } from '../config/database.js'; import { db } from '../config/database.js';
import { users, userEmailAccounts, emailAccounts } from '../db/schema.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 { hashPassword, generateTempPassword } from '../utils/password.js';
import { ConflictError, NotFoundError } from '../utils/errors.js'; import { ConflictError, NotFoundError } from '../utils/errors.js';
import * as emailAccountService from './email-account.service.js'; import * as emailAccountService from './email-account.service.js';
@@ -162,6 +162,74 @@ export const changeUserRole = async (userId, newRole) => {
return { userId, oldRole, 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 * Zmazanie usera
*/ */
@@ -179,11 +247,11 @@ export const deleteUser = async (userId) => {
// Zabraň zmazaniu posledného admina // Zabraň zmazaniu posledného admina
if (user.role === 'admin') { if (user.role === 'admin') {
const [adminCount] = await db const [adminCount] = await db
.select({ count: db.$count(users) }) .select({ count: count() })
.from(users) .from(users)
.where(eq(users.role, 'admin')); .where(eq(users.role, 'admin'));
if (adminCount.count <= 1) { if ((adminCount?.count || 0) <= 1) {
throw new ConflictError('Nemôžete zmazať posledného administrátora'); throw new ConflictError('Nemôžete zmazať posledného administrátora');
} }
} }
@@ -203,11 +271,11 @@ export const deleteUser = async (userId) => {
let deletedEmailAccounts = 0; let deletedEmailAccounts = 0;
for (const emailAccountId of emailAccountIds) { for (const emailAccountId of emailAccountIds) {
const [remainingLinks] = await db const [remainingLinks] = await db
.select({ count: db.$count(userEmailAccounts) }) .select({ count: count() })
.from(userEmailAccounts) .from(userEmailAccounts)
.where(eq(userEmailAccounts.emailAccountId, emailAccountId)); .where(eq(userEmailAccounts.emailAccountId, emailAccountId));
if (remainingLinks.count === 0) { if ((remainingLinks?.count || 0) === 0) {
await db.delete(emailAccounts).where(eq(emailAccounts.id, emailAccountId)); await db.delete(emailAccounts).where(eq(emailAccounts.id, emailAccountId));
deletedEmailAccounts++; deletedEmailAccounts++;
} }

View File

@@ -32,7 +32,7 @@ export const getAllCompanies = async (searchTerm = null, userId = null, userRole
phone: companies.phone, phone: companies.phone,
email: companies.email, email: companies.email,
website: companies.website, website: companies.website,
isActive: companies.isActive, status: companies.status,
createdBy: companies.createdBy, createdBy: companies.createdBy,
createdAt: companies.createdAt, createdAt: companies.createdAt,
updatedAt: companies.updatedAt, updatedAt: companies.updatedAt,
@@ -85,7 +85,7 @@ export const getCompanyById = async (companyId) => {
phone: companies.phone, phone: companies.phone,
email: companies.email, email: companies.email,
website: companies.website, website: companies.website,
isActive: companies.isActive, status: companies.status,
createdBy: companies.createdBy, createdBy: companies.createdBy,
createdAt: companies.createdAt, createdAt: companies.createdAt,
updatedAt: companies.updatedAt, updatedAt: companies.updatedAt,
@@ -110,7 +110,7 @@ export const getCompanyById = async (companyId) => {
* Create new company * Create new company
*/ */
export const createCompany = async (userId, data) => { 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 // Check if company with same name already exists
const [existing] = await db const [existing] = await db
@@ -134,6 +134,7 @@ export const createCompany = async (userId, data) => {
phone: phone || null, phone: phone || null,
email: email || null, email: email || null,
website: website || null, website: website || null,
status: status || 'registered',
createdBy: userId, createdBy: userId,
}) })
.returning(); .returning();
@@ -147,7 +148,7 @@ export const createCompany = async (userId, data) => {
export const updateCompany = async (companyId, data) => { export const updateCompany = async (companyId, data) => {
const company = await getCompanyById(companyId); 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 is being changed, check for duplicates
if (name && name !== company.name) { if (name && name !== company.name) {
@@ -173,7 +174,7 @@ export const updateCompany = async (companyId, data) => {
phone: phone !== undefined ? phone : company.phone, phone: phone !== undefined ? phone : company.phone,
email: email !== undefined ? email : company.email, email: email !== undefined ? email : company.email,
website: website !== undefined ? website : company.website, website: website !== undefined ? website : company.website,
isActive: isActive !== undefined ? isActive : company.isActive, status: status !== undefined ? status : company.status,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(companies.id, companyId)) .where(eq(companies.id, companyId))

View File

@@ -14,6 +14,7 @@ export const listPersonalContacts = async (userId) => {
phone: personalContacts.phone, phone: personalContacts.phone,
email: personalContacts.email, email: personalContacts.email,
secondaryEmail: personalContacts.secondaryEmail, secondaryEmail: personalContacts.secondaryEmail,
description: personalContacts.description,
createdAt: personalContacts.createdAt, createdAt: personalContacts.createdAt,
updatedAt: personalContacts.updatedAt, updatedAt: personalContacts.updatedAt,
companyName: companies.name, companyName: companies.name,
@@ -69,6 +70,7 @@ export const createPersonalContact = async (userId, contact) => {
phone: contact.phone, phone: contact.phone,
email: contact.email, email: contact.email,
secondaryEmail: contact.secondaryEmail || null, secondaryEmail: contact.secondaryEmail || null,
description: contact.description || null,
}) })
.returning() .returning()
@@ -104,6 +106,7 @@ export const updatePersonalContact = async (contactId, userId, updates) => {
if (updates.email !== undefined) updateData.email = updates.email if (updates.email !== undefined) updateData.email = updates.email
if (updates.secondaryEmail !== undefined) updateData.secondaryEmail = updates.secondaryEmail if (updates.secondaryEmail !== undefined) updateData.secondaryEmail = updates.secondaryEmail
if (updates.companyId !== undefined) updateData.companyId = updates.companyId || null if (updates.companyId !== undefined) updateData.companyId = updates.companyId || null
if (updates.description !== undefined) updateData.description = updates.description || null
const [updated] = await db const [updated] = await db
.update(personalContacts) .update(personalContacts)

View File

@@ -15,6 +15,7 @@ export const createCompanySchema = z.object({
phone: z.string().max(50).optional(), phone: z.string().max(50).optional(),
email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')), 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('')), 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({ export const updateCompanySchema = z.object({
@@ -26,7 +27,7 @@ export const updateCompanySchema = z.object({
phone: z.string().max(50).optional(), phone: z.string().max(50).optional(),
email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')), 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('')), 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 // Project validators
@@ -176,7 +177,7 @@ export const updateTimeEntrySchema = z.object({
export const createEventSchema = z.object({ export const createEventSchema = z.object({
title: z.string().min(1, 'Názov je povinný'), title: z.string().min(1, 'Názov je povinný'),
description: z.string().optional(), 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ý'), start: z.string().min(1, 'Začiatok je povinný'),
end: z.string().min(1, 'Koniec je povinný'), end: z.string().min(1, 'Koniec je povinný'),
assignedUserIds: z.array(z.string().uuid('Neplatný formát user ID')).optional(), 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({ export const updateEventSchema = z.object({
title: z.string().min(1).optional(), title: z.string().min(1).optional(),
description: z.string().optional(), description: z.string().optional(),
type: z.enum(['meeting', 'event']).optional(), type: z.enum(['meeting', 'event', 'important']).optional(),
start: z.string().optional(), start: z.string().optional(),
end: z.string().optional(), end: z.string().optional(),
assignedUserIds: z.array(z.string().uuid('Neplatný formát user ID')).optional(), assignedUserIds: z.array(z.string().uuid('Neplatný formát user ID')).optional(),