Správa používateľov + notifikačný systém

- Pridaná kompletná správa používateľov (CRUD, reset hesla, zmena roly) pre ROOT/ADMIN
- Backend: POST /users endpoint, createUser controller, validácia
- Frontend: UserManagement, UserForm, PasswordResetModal komponenty
- Settings prístupné pre ROOT aj ADMIN (AdminRoute)
- Notifikačný systém s snooze funkcionalitou
- Aktualizácia HELPDESK_INIT_V2.md dokumentácie

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 15:30:27 +01:00
parent cbdd952bc1
commit 2ca0c4f4d8
36 changed files with 3116 additions and 522 deletions

View File

@@ -67,6 +67,7 @@ model User {
taskAttachments TaskAttachment[] @relation("TaskAttachmentUploader")
createdCustomers Customer[]
notifications Notification[]
@@index([email])
@@index([roleId])
@@ -374,11 +375,12 @@ model Task {
createdById String
createdBy User @relation("TaskCreator", fields: [createdById], references: [id])
assignees TaskAssignee[]
reminders Reminder[]
comments Comment[]
tags TaskTag[]
attachments TaskAttachment[]
assignees TaskAssignee[]
reminders Reminder[]
comments Comment[]
tags TaskTag[]
attachments TaskAttachment[]
notifications Notification[]
@@index([projectId])
@@index([parentId])
@@ -633,6 +635,7 @@ model RMA {
statusHistory RMAStatusHistory[]
comments RMAComment[]
tags RMATag[]
notifications Notification[]
@@index([rmaNumber])
@@index([customerId])
@@ -706,6 +709,40 @@ model RMATag {
@@id([rmaId, tagId])
}
// ==================== NOTIFICATIONS ====================
model Notification {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String // TASK_ASSIGNED, TASK_STATUS_CHANGED, TASK_COMMENT, TASK_DEADLINE, RMA_ASSIGNED, etc.
taskId String?
task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade)
rmaId String?
rma RMA? @relation(fields: [rmaId], references: [id], onDelete: Cascade)
title String
message String
data Json? // Extra data (oldStatus, newStatus, etc.)
isRead Boolean @default(false)
readAt DateTime?
snoozedUntil DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([userId, isRead])
@@index([taskId])
@@index([rmaId])
@@index([createdAt])
}
// ==================== ACTIVITY LOG ====================
model ActivityLog {

View File

@@ -203,6 +203,19 @@ async function seed() {
label: 'Zapnúť real-time aktualizácie (WebSocket)',
dataType: 'boolean',
},
{
key: 'NOTIFICATION_SNOOZE_OPTIONS',
value: [
{ label: '30 minút', minutes: 30 },
{ label: '1 hodina', minutes: 60 },
{ label: '3 hodiny', minutes: 180 },
{ label: 'Zajtra ráno', type: 'tomorrow', hour: 9 },
],
category: 'NOTIFICATIONS',
label: 'Možnosti odloženia notifikácií',
description: 'Pole objektov. Každý má "label" a buď "minutes" (relatívny čas) alebo "type" + "hour" (konkrétny čas). Type: "today" (ak čas prešiel, skryje sa), "tomorrow".',
dataType: 'json',
},
],
});

View File

@@ -0,0 +1,100 @@
import { Response } from 'express';
import { AuthRequest } from '../middleware/auth.middleware';
import { notificationService } from '../services/notification.service';
import { successResponse, errorResponse, parseQueryInt, parseQueryBoolean, getParam } from '../utils/helpers';
// Get notifications for current user
export const getNotifications = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const limit = parseQueryInt(req.query.limit, 50);
const offset = parseQueryInt(req.query.offset, 0);
const unreadOnly = parseQueryBoolean(req.query.unreadOnly, false);
const { notifications, total } = await notificationService.getForUser(userId, {
limit,
offset,
unreadOnly,
});
successResponse(res, { notifications, total, limit, offset });
} catch (error) {
console.error('[Notification] Error getting notifications:', error);
errorResponse(res, 'Chyba pri načítaní notifikácií', 500);
}
};
// Get unread count
export const getUnreadCount = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const count = await notificationService.getUnreadCount(userId);
successResponse(res, { count });
} catch (error) {
console.error('[Notification] Error getting unread count:', error);
errorResponse(res, 'Chyba pri načítaní počtu notifikácií', 500);
}
};
// Mark notification as read
export const markAsRead = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const id = getParam(req, 'id');
await notificationService.markAsRead(id, userId);
successResponse(res, { message: 'Notifikácia označená ako prečítaná' });
} catch (error) {
console.error('[Notification] Error marking as read:', error);
errorResponse(res, 'Chyba pri označovaní notifikácie', 500);
}
};
// Mark all notifications as read
export const markAllAsRead = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const result = await notificationService.markAllAsRead(userId);
successResponse(res, { message: 'Všetky notifikácie označené ako prečítané', count: result.count });
} catch (error) {
console.error('[Notification] Error marking all as read:', error);
errorResponse(res, 'Chyba pri označovaní notifikácií', 500);
}
};
// Snooze notification (remind later)
export const snoozeNotification = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const id = getParam(req, 'id');
const { minutes } = req.body;
if (!minutes || typeof minutes !== 'number' || minutes <= 0) {
errorResponse(res, 'Neplatný čas odloženia', 400);
return;
}
const until = new Date();
until.setMinutes(until.getMinutes() + minutes);
await notificationService.snooze(id, userId, until);
successResponse(res, { message: 'Notifikácia odložená', snoozedUntil: until });
} catch (error) {
console.error('[Notification] Error snoozing:', error);
errorResponse(res, 'Chyba pri odkladaní notifikácie', 500);
}
};
// Delete notification
export const deleteNotification = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user!.userId;
const id = getParam(req, 'id');
await notificationService.delete(id, userId);
successResponse(res, { message: 'Notifikácia vymazaná' });
} catch (error) {
console.error('[Notification] Error deleting:', error);
errorResponse(res, 'Chyba pri mazaní notifikácie', 500);
}
};

View File

@@ -4,6 +4,7 @@ import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getPa
import { AuthRequest } from '../middleware/auth.middleware';
import { configService } from '../services/config.service';
import { movePendingFilesToEntity } from './upload.controller';
import { notificationService } from '../services/notification.service';
export const getTasks = async (req: AuthRequest, res: Response): Promise<void> => {
try {
@@ -167,6 +168,13 @@ export const createTask = async (req: AuthRequest, res: Response): Promise<void>
userId,
})),
});
// Notify assigned users
await notificationService.notifyTaskAssignment(
task.id,
req.body.assigneeIds,
req.user!.userId
);
}
// Move pending files if tempId provided
@@ -189,13 +197,22 @@ export const updateTask = async (req: AuthRequest, res: Response): Promise<void>
try {
const id = getParam(req, 'id');
const existing = await prisma.task.findUnique({ where: { id } });
const existing = await prisma.task.findUnique({
where: { id },
include: {
status: { select: { name: true } },
assignees: { select: { userId: true } },
},
});
if (!existing) {
errorResponse(res, 'Úloha nebola nájdená.', 404);
return;
}
const oldStatusName = existing.status.name;
const oldAssigneeIds = existing.assignees.map((a) => a.userId);
const updateData: Record<string, unknown> = {};
if (req.body.title) updateData.title = req.body.title;
if (req.body.description !== undefined) updateData.description = req.body.description;
@@ -245,6 +262,26 @@ export const updateTask = async (req: AuthRequest, res: Response): Promise<void>
},
});
// Notify about status change
if (req.body.statusId && updatedTask && oldStatusName !== updatedTask.status.name) {
await notificationService.notifyTaskStatusChange(
id,
oldStatusName,
updatedTask.status.name,
req.user!.userId
);
}
// Notify new assignees
if (req.body.assigneeIds !== undefined) {
const newAssigneeIds = (req.body.assigneeIds || []).filter(
(userId: string) => !oldAssigneeIds.includes(userId)
);
if (newAssigneeIds.length > 0) {
await notificationService.notifyTaskAssignment(id, newAssigneeIds, req.user!.userId);
}
}
if (req.logActivity) {
await req.logActivity('UPDATE', 'Task', id, updateData);
}
@@ -285,17 +322,40 @@ export const updateTaskStatus = async (req: AuthRequest, res: Response): Promise
const id = getParam(req, 'id');
const { statusId } = req.body;
const status = await prisma.taskStatus.findUnique({ where: { id: statusId } });
// Get current task with old status
const currentTask = await prisma.task.findUnique({
where: { id },
include: { status: true },
});
if (!currentTask) {
errorResponse(res, 'Úloha nebola nájdená.', 404);
return;
}
const oldStatusName = currentTask.status.name;
const newStatus = await prisma.taskStatus.findUnique({ where: { id: statusId } });
const task = await prisma.task.update({
where: { id },
data: {
statusId,
completedAt: status?.isFinal ? new Date() : null,
completedAt: newStatus?.isFinal ? new Date() : null,
},
include: { status: true },
});
// Notify about status change
if (oldStatusName !== task.status.name) {
await notificationService.notifyTaskStatusChange(
id,
oldStatusName,
task.status.name,
req.user!.userId
);
}
if (req.logActivity) {
await req.logActivity('STATUS_CHANGE', 'Task', id, { statusId });
}
@@ -398,6 +458,9 @@ export const addTaskComment = async (req: AuthRequest, res: Response): Promise<v
},
});
// Notify about new comment
await notificationService.notifyTaskComment(id, comment.id, userId, comment.user.name);
successResponse(res, comment, 'Komentár bol pridaný.', 201);
} catch (error) {
console.error('Error adding task comment:', error);

View File

@@ -40,6 +40,54 @@ export const getUsersSimple = async (req: AuthRequest, res: Response): Promise<v
}
};
export const createUser = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { email, password, name, roleId } = req.body;
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
errorResponse(res, 'Email je už používaný.', 409);
return;
}
const role = await prisma.userRole.findUnique({ where: { id: roleId } });
if (!role || !role.active) {
errorResponse(res, 'Rola neexistuje alebo nie je aktívna.', 404);
return;
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name, roleId },
select: {
id: true,
email: true,
name: true,
active: true,
createdAt: true,
updatedAt: true,
role: {
select: {
id: true,
code: true,
name: true,
},
},
},
});
if (req.logActivity) {
await req.logActivity('CREATE', 'User', user.id, { email, name, roleId });
}
successResponse(res, user, 'Používateľ bol vytvorený.', 201);
} catch (error) {
console.error('Error creating user:', error);
errorResponse(res, 'Chyba pri vytváraní používateľa.', 500);
}
};
export const getUsers = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const page = parseQueryInt(req.query.page, 1);

View File

@@ -20,6 +20,8 @@ export const getZakazky = async (req: AuthRequest, res: Response): Promise<void>
const rok = parseQueryInt(req.query.rok, new Date().getFullYear());
const search = getQueryString(req, 'search');
console.log(`[Zakazky] Fetching year=${rok}, search=${search || 'none'}`);
let zakazky;
if (search) {
@@ -28,10 +30,16 @@ export const getZakazky = async (req: AuthRequest, res: Response): Promise<void>
zakazky = await externalDbService.getZakazkyByYear(rok);
}
console.log(`[Zakazky] Found ${zakazky.length} records`);
successResponse(res, zakazky);
} catch (error) {
console.error('Error fetching zakazky:', error);
errorResponse(res, 'Chyba pri načítaní zákaziek z externej databázy.', 500);
const err = error as Error;
console.error('[Zakazky] Error:', err.message, err.stack);
// Return detailed error in development
const message = process.env.NODE_ENV === 'development'
? `Chyba: ${err.message}`
: 'Chyba pri načítaní zákaziek z externej databázy.';
errorResponse(res, message, 500);
}
};

View File

@@ -10,6 +10,7 @@ import settingsRoutes from './settings.routes';
import dashboardRoutes from './dashboard.routes';
import uploadRoutes from './upload.routes';
import zakazkyRoutes from './zakazky.routes';
import notificationRoutes from './notification.routes';
const router = Router();
@@ -24,5 +25,6 @@ router.use('/settings', settingsRoutes);
router.use('/dashboard', dashboardRoutes);
router.use('/files', uploadRoutes);
router.use('/zakazky', zakazkyRoutes);
router.use('/notifications', notificationRoutes);
export default router;

View File

@@ -0,0 +1,35 @@
import { Router } from 'express';
import { authenticate } from '../middleware/auth.middleware';
import {
getNotifications,
getUnreadCount,
markAsRead,
markAllAsRead,
snoozeNotification,
deleteNotification,
} from '../controllers/notification.controller';
const router = Router();
// All routes require authentication
router.use(authenticate);
// GET /api/notifications - Get user's notifications
router.get('/', getNotifications);
// GET /api/notifications/unread-count - Get unread count
router.get('/unread-count', getUnreadCount);
// POST /api/notifications/mark-all-read - Mark all as read
router.post('/mark-all-read', markAllAsRead);
// POST /api/notifications/:id/read - Mark single notification as read
router.post('/:id/read', markAsRead);
// POST /api/notifications/:id/snooze - Snooze notification
router.post('/:id/snooze', snoozeNotification);
// DELETE /api/notifications/:id - Delete notification
router.delete('/:id', deleteNotification);
export default router;

View File

@@ -25,6 +25,10 @@ router.get('/rma-solutions', settingsController.getRMASolutions);
router.get('/tags', settingsController.getTags);
// User Roles - čítanie
router.get('/roles', settingsController.getUserRoles);
// System Settings - čítanie (pre všetkých prihlásených)
router.get('/system', settingsController.getSystemSettings);
router.get('/system/category/:category', settingsController.getSystemSettingsByCategory);
router.get('/system/:key', settingsController.getSystemSetting);
// === ROOT-ONLY ENDPOINTY (vytvorenie, úprava, mazanie) ===
router.use(isRoot);
@@ -69,10 +73,7 @@ router.post('/roles', settingsController.createUserRole);
router.put('/roles/:id', settingsController.updateUserRole);
router.delete('/roles/:id', settingsController.deleteUserRole);
// System Settings - len ROOT
router.get('/system', settingsController.getSystemSettings);
router.get('/system/category/:category', settingsController.getSystemSettingsByCategory);
router.get('/system/:key', settingsController.getSystemSetting);
// System Settings - úprava (len ROOT)
router.put('/system/:key', settingsController.updateSystemSetting);
export default router;

View File

@@ -3,6 +3,8 @@ import * as usersController from '../controllers/users.controller';
import { authenticate } from '../middleware/auth.middleware';
import { isAdmin } from '../middleware/rbac.middleware';
import { activityLogger } from '../middleware/activityLog.middleware';
import { validate } from '../middleware/validate.middleware';
import { createUserSchema } from '../utils/validators';
const router = Router();
@@ -13,6 +15,7 @@ router.use(activityLogger);
router.get('/simple', usersController.getUsersSimple);
router.get('/', isAdmin, usersController.getUsers);
router.post('/', isAdmin, validate(createUserSchema), usersController.createUser);
router.get('/:id', isAdmin, usersController.getUser);
router.put('/:id', isAdmin, usersController.updateUser);
router.delete('/:id', isAdmin, usersController.deleteUser);

View File

@@ -144,15 +144,19 @@ export const getZakazkaById = async (rok: number, id: number): Promise<Zakazka |
return zakazky.find((z) => z.id === id) || null;
};
// Normalize text for search (remove diacritics, lowercase)
const normalizeText = (text: string): string =>
text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
// Search zakazky
export const searchZakazky = async (rok: number, search: string): Promise<Zakazka[]> => {
const zakazky = await getZakazkyByYear(rok);
const searchLower = search.toLowerCase();
const searchNormalized = normalizeText(search);
return zakazky.filter((z) =>
z.cislo.toLowerCase().includes(searchLower) ||
z.nazov.toLowerCase().includes(searchLower) ||
z.customer.toLowerCase().includes(searchLower)
normalizeText(z.cislo).includes(searchNormalized) ||
normalizeText(z.nazov).includes(searchNormalized) ||
normalizeText(z.customer).includes(searchNormalized)
);
};

View File

@@ -0,0 +1,324 @@
import prisma from '../config/database';
import { Prisma } from '@prisma/client';
export enum NotificationType {
TASK_ASSIGNED = 'TASK_ASSIGNED',
TASK_STATUS_CHANGED = 'TASK_STATUS_CHANGED',
TASK_COMMENT = 'TASK_COMMENT',
TASK_DEADLINE_APPROACHING = 'TASK_DEADLINE_APPROACHING',
TASK_UPDATED = 'TASK_UPDATED',
RMA_ASSIGNED = 'RMA_ASSIGNED',
RMA_STATUS_CHANGED = 'RMA_STATUS_CHANGED',
RMA_COMMENT = 'RMA_COMMENT',
}
interface CreateNotificationData {
userId: string;
type: NotificationType;
title: string;
message: string;
taskId?: string;
rmaId?: string;
data?: Prisma.InputJsonValue;
}
export const notificationService = {
// Create a new notification
async create(data: CreateNotificationData) {
return prisma.notification.create({
data: {
userId: data.userId,
type: data.type,
title: data.title,
message: data.message,
taskId: data.taskId,
rmaId: data.rmaId,
data: data.data || undefined,
},
});
},
// Create notifications for multiple users
async createForUsers(userIds: string[], data: Omit<CreateNotificationData, 'userId'>) {
if (userIds.length === 0) return [];
return prisma.notification.createMany({
data: userIds.map((userId) => ({
userId,
type: data.type,
title: data.title,
message: data.message,
taskId: data.taskId,
rmaId: data.rmaId,
data: data.data || undefined,
})),
});
},
// Get notifications for a user
async getForUser(userId: string, options?: { limit?: number; offset?: number; unreadOnly?: boolean }) {
const { limit = 50, offset = 0, unreadOnly = false } = options || {};
const where = {
userId,
...(unreadOnly ? { isRead: false } : {}),
OR: [
{ snoozedUntil: null },
{ snoozedUntil: { lte: new Date() } },
],
};
const [rawNotifications, total] = await Promise.all([
prisma.notification.findMany({
where,
orderBy: { createdAt: 'desc' },
take: limit,
skip: offset,
include: {
task: {
select: {
id: true,
title: true,
status: { select: { id: true, name: true, color: true } },
project: { select: { id: true, name: true } },
},
},
rma: {
select: {
id: true,
rmaNumber: true,
productName: true,
},
},
},
}),
prisma.notification.count({ where }),
]);
// Pre TASK_COMMENT notifikácie načítaj text komentára z Comment tabuľky
const notifications = await Promise.all(
rawNotifications.map(async (notification) => {
if (notification.type === 'TASK_COMMENT' && notification.taskId) {
const data = notification.data as { commentId?: string; actorName?: string } | null;
let comment;
if (data?.commentId) {
// Nové notifikácie - načítaj podľa commentId
comment = await prisma.comment.findUnique({
where: { id: data.commentId },
select: { content: true, user: { select: { name: true } } },
});
} else {
// Staré notifikácie - nájdi podľa času (±5 sekúnd)
const notifTime = notification.createdAt.getTime();
comment = await prisma.comment.findFirst({
where: {
taskId: notification.taskId,
createdAt: {
gte: new Date(notifTime - 5000),
lte: new Date(notifTime + 5000),
},
},
select: { content: true, user: { select: { name: true } } },
});
}
if (comment) {
const shortComment = comment.content.length > 100
? comment.content.substring(0, 100) + '...'
: comment.content;
return {
...notification,
message: shortComment,
data: { ...data, actorName: data?.actorName || comment.user?.name },
};
}
}
return notification;
})
);
return { notifications, total };
},
// Get unread count for a user
async getUnreadCount(userId: string) {
return prisma.notification.count({
where: {
userId,
isRead: false,
OR: [
{ snoozedUntil: null },
{ snoozedUntil: { lte: new Date() } },
],
},
});
},
// Mark notification as read
async markAsRead(notificationId: string, userId: string) {
return prisma.notification.updateMany({
where: {
id: notificationId,
userId, // Security: only owner can mark as read
},
data: {
isRead: true,
readAt: new Date(),
},
});
},
// Mark all notifications as read for a user
async markAllAsRead(userId: string) {
return prisma.notification.updateMany({
where: {
userId,
isRead: false,
},
data: {
isRead: true,
readAt: new Date(),
},
});
},
// Snooze notification (remind later)
async snooze(notificationId: string, userId: string, until: Date) {
return prisma.notification.updateMany({
where: {
id: notificationId,
userId,
},
data: {
snoozedUntil: until,
},
});
},
// Delete notification
async delete(notificationId: string, userId: string) {
return prisma.notification.deleteMany({
where: {
id: notificationId,
userId,
},
});
},
// Delete old notifications (cleanup job)
async deleteOld(olderThanDays: number = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
return prisma.notification.deleteMany({
where: {
isRead: true,
createdAt: { lt: cutoffDate },
},
});
},
// Helper: Notify task assignees about status change
async notifyTaskStatusChange(
taskId: string,
oldStatusName: string,
newStatusName: string,
changedByUserId: string
) {
const task = await prisma.task.findUnique({
where: { id: taskId },
include: {
assignees: { select: { userId: true } },
createdBy: { select: { id: true, name: true } },
},
});
if (!task) return;
// Get actor name
const actor = await prisma.user.findUnique({
where: { id: changedByUserId },
select: { name: true },
});
// Notify assignees (except the one who made the change)
const userIds = task.assignees
.map((a: { userId: string }) => a.userId)
.filter((id: string) => id !== changedByUserId);
// Also notify task creator if not already in list
if (task.createdById !== changedByUserId && !userIds.includes(task.createdById)) {
userIds.push(task.createdById);
}
if (userIds.length === 0) return;
await this.createForUsers(userIds, {
type: NotificationType.TASK_STATUS_CHANGED,
title: 'Zmena stavu úlohy',
message: `${oldStatusName}${newStatusName}`,
taskId: task.id,
data: { oldStatus: oldStatusName, newStatus: newStatusName, actorName: actor?.name },
});
},
// Helper: Notify user about task assignment
async notifyTaskAssignment(taskId: string, assignedUserIds: string[], assignedByUserId: string) {
const task = await prisma.task.findUnique({
where: { id: taskId },
select: { id: true, title: true },
});
if (!task) return;
// Get actor name
const actor = await prisma.user.findUnique({
where: { id: assignedByUserId },
select: { name: true },
});
// Don't notify the user who assigned (if they assigned themselves)
const userIds = assignedUserIds.filter((id) => id !== assignedByUserId);
if (userIds.length === 0) return;
await this.createForUsers(userIds, {
type: NotificationType.TASK_ASSIGNED,
title: 'Nová úloha',
message: 'Boli ste priradení k úlohe',
taskId: task.id,
data: { actorName: actor?.name },
});
},
// Helper: Notify about new comment on task
async notifyTaskComment(taskId: string, commentId: string, commentByUserId: string, commentByUserName: string) {
const task = await prisma.task.findUnique({
where: { id: taskId },
include: {
assignees: { select: { userId: true } },
createdBy: { select: { id: true } },
},
});
if (!task) return;
// Notify assignees and creator (except comment author)
const userIds = new Set<string>();
task.assignees.forEach((a: { userId: string }) => userIds.add(a.userId));
userIds.add(task.createdById);
userIds.delete(commentByUserId);
if (userIds.size === 0) return;
await this.createForUsers(Array.from(userIds), {
type: NotificationType.TASK_COMMENT,
title: 'Nový komentár',
message: '', // Text sa načíta z Comment tabuľky
taskId: task.id,
data: { commentId, actorName: commentByUserName },
});
},
};

View File

@@ -71,3 +71,9 @@ export const parseBooleanQuery = (value: unknown): boolean | undefined => {
if (value === 'false') return false;
return undefined;
};
export const parseQueryBoolean = (value: unknown, defaultValue: boolean): boolean => {
if (value === 'true' || value === '1') return true;
if (value === 'false' || value === '0') return false;
return defaultValue;
};

View File

@@ -18,6 +18,17 @@ export const registerSchema = z.object({
});
// User validators
export const createUserSchema = z.object({
email: z.string().email('Neplatný email'),
password: z
.string()
.min(8, 'Heslo musí mať aspoň 8 znakov')
.regex(/[A-Z]/, 'Heslo musí obsahovať veľké písmeno')
.regex(/[0-9]/, 'Heslo musí obsahovať číslo'),
name: z.string().min(2, 'Meno musí mať aspoň 2 znaky'),
roleId: z.string().min(1, 'Rola je povinná'),
});
export const updateUserSchema = z.object({
email: z.string().email('Neplatný email').optional(),
name: z.string().min(2, 'Meno musí mať aspoň 2 znaky').optional(),