Initial commit: Helpdesk application setup
- Backend: Node.js/TypeScript with Prisma ORM - Frontend: Vite + TypeScript - Project configuration and documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
17
backend/src/config/database.ts
Normal file
17
backend/src/config/database.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
export default prisma;
|
||||
29
backend/src/config/env.ts
Normal file
29
backend/src/config/env.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const env = {
|
||||
// Server
|
||||
PORT: parseInt(process.env.PORT || '3001', 10),
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
|
||||
// Database
|
||||
DATABASE_URL: process.env.DATABASE_URL || '',
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'default-secret',
|
||||
JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '15m',
|
||||
JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
||||
|
||||
// Frontend
|
||||
FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
|
||||
// Upload
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR || './uploads',
|
||||
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || '10485760', 10),
|
||||
|
||||
// Helpers
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
isProd: process.env.NODE_ENV === 'production',
|
||||
};
|
||||
29
backend/src/config/jwt.ts
Normal file
29
backend/src/config/jwt.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from './env';
|
||||
|
||||
export interface TokenPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
roleCode: string;
|
||||
}
|
||||
|
||||
export const generateAccessToken = (payload: TokenPayload): string => {
|
||||
return jwt.sign(payload, env.JWT_SECRET, {
|
||||
expiresIn: '15m',
|
||||
});
|
||||
};
|
||||
|
||||
export const generateRefreshToken = (payload: TokenPayload): string => {
|
||||
return jwt.sign(payload, env.JWT_REFRESH_SECRET, {
|
||||
expiresIn: '7d',
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyAccessToken = (token: string): TokenPayload => {
|
||||
return jwt.verify(token, env.JWT_SECRET) as TokenPayload;
|
||||
};
|
||||
|
||||
export const verifyRefreshToken = (token: string): TokenPayload => {
|
||||
return jwt.verify(token, env.JWT_REFRESH_SECRET) as TokenPayload;
|
||||
};
|
||||
131
backend/src/controllers/auth.controller.ts
Normal file
131
backend/src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { authService } from '../services/auth.service';
|
||||
import { successResponse, errorResponse } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { logActivity } from '../middleware/activityLog.middleware';
|
||||
|
||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const user = await authService.register(req.body);
|
||||
|
||||
await logActivity(
|
||||
user.id,
|
||||
'CREATE',
|
||||
'User',
|
||||
user.id,
|
||||
{ email: user.email, name: user.name },
|
||||
req.ip,
|
||||
req.get('User-Agent')
|
||||
);
|
||||
|
||||
successResponse(res, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
}, 'Registrácia úspešná.', 201);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorResponse(res, error.message, 400);
|
||||
} else {
|
||||
errorResponse(res, 'Chyba pri registrácii.', 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { user, tokens } = await authService.login(req.body);
|
||||
|
||||
await logActivity(
|
||||
user.id,
|
||||
'LOGIN',
|
||||
'User',
|
||||
user.id,
|
||||
{ email: user.email },
|
||||
req.ip,
|
||||
req.get('User-Agent')
|
||||
);
|
||||
|
||||
successResponse(res, {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
},
|
||||
...tokens,
|
||||
}, 'Prihlásenie úspešné.');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorResponse(res, error.message, 401);
|
||||
} else {
|
||||
errorResponse(res, 'Chyba pri prihlásení.', 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const refresh = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
errorResponse(res, 'Refresh token je povinný.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = await authService.refreshTokens(refreshToken);
|
||||
|
||||
successResponse(res, tokens, 'Tokeny obnovené.');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorResponse(res, error.message, 401);
|
||||
} else {
|
||||
errorResponse(res, 'Chyba pri obnove tokenov.', 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const logout = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (req.user) {
|
||||
await logActivity(
|
||||
req.user.userId,
|
||||
'LOGOUT',
|
||||
'User',
|
||||
req.user.userId,
|
||||
{ email: req.user.email },
|
||||
req.ip,
|
||||
req.get('User-Agent')
|
||||
);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Odhlásenie úspešné.');
|
||||
} catch {
|
||||
errorResponse(res, 'Chyba pri odhlásení.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMe = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
errorResponse(res, 'Nie ste prihlásený.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authService.getMe(req.user.userId);
|
||||
|
||||
successResponse(res, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorResponse(res, error.message, 404);
|
||||
} else {
|
||||
errorResponse(res, 'Chyba pri získaní údajov.', 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
216
backend/src/controllers/customers.controller.ts
Normal file
216
backend/src/controllers/customers.controller.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
export const getCustomers = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const active = getQueryString(req, 'active');
|
||||
|
||||
const where = {
|
||||
...(active !== undefined && { active: active === 'true' }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ email: { contains: search, mode: 'insensitive' as const } },
|
||||
{ ico: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [customers, total] = await Promise.all([
|
||||
prisma.customer.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
projects: true,
|
||||
equipment: true,
|
||||
rmas: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.customer.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, customers, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customers:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zákazníkov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomer = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
createdBy: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
projects: true,
|
||||
equipment: true,
|
||||
rmas: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
errorResponse(res, 'Zákazník nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, customer);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customer:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zákazníka.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createCustomer = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const customer = await prisma.customer.create({
|
||||
data: {
|
||||
...req.body,
|
||||
createdById: req.user!.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Customer', customer.id, { name: customer.name });
|
||||
}
|
||||
|
||||
successResponse(res, customer, 'Zákazník bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating customer:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní zákazníka.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCustomer = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const existing = await prisma.customer.findUnique({ where: { id } });
|
||||
|
||||
if (!existing) {
|
||||
errorResponse(res, 'Zákazník nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const customer = await prisma.customer.update({
|
||||
where: { id },
|
||||
data: req.body,
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'Customer', id, req.body);
|
||||
}
|
||||
|
||||
successResponse(res, customer, 'Zákazník bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating customer:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii zákazníka.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCustomer = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const customer = await prisma.customer.findUnique({ where: { id } });
|
||||
|
||||
if (!customer) {
|
||||
errorResponse(res, 'Zákazník nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await prisma.customer.update({
|
||||
where: { id },
|
||||
data: { active: false },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'Customer', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Zákazník bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting customer:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní zákazníka.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomerProjects = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { customerId: id },
|
||||
include: {
|
||||
status: true,
|
||||
owner: { select: { id: true, name: true } },
|
||||
_count: { select: { tasks: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
successResponse(res, projects);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customer projects:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní projektov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomerEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const equipment = await prisma.equipment.findMany({
|
||||
where: { customerId: id, active: true },
|
||||
include: {
|
||||
type: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
successResponse(res, equipment);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customer equipment:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zariadení.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomerRMAs = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const rmas = await prisma.rMA.findMany({
|
||||
where: { customerId: id },
|
||||
include: {
|
||||
status: true,
|
||||
proposedSolution: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
successResponse(res, rmas);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customer RMAs:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní reklamácií.', 500);
|
||||
}
|
||||
};
|
||||
333
backend/src/controllers/dashboard.controller.ts
Normal file
333
backend/src/controllers/dashboard.controller.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
// Hlavný dashboard endpoint - štatistiky pre karty
|
||||
export const getDashboard = async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const nextMonth = new Date(today);
|
||||
nextMonth.setDate(nextMonth.getDate() + 30);
|
||||
|
||||
const [
|
||||
totalProjects,
|
||||
activeProjects,
|
||||
totalTasks,
|
||||
pendingTasks,
|
||||
inProgressTasks,
|
||||
totalCustomers,
|
||||
activeCustomers,
|
||||
totalEquipment,
|
||||
upcomingRevisions,
|
||||
totalRMAs,
|
||||
pendingRMAs,
|
||||
] = await Promise.all([
|
||||
prisma.project.count(),
|
||||
prisma.project.count({ where: { status: { isFinal: false } } }),
|
||||
prisma.task.count(),
|
||||
prisma.task.count({ where: { status: { code: 'NEW' } } }),
|
||||
prisma.task.count({ where: { status: { code: 'IN_PROGRESS' } } }),
|
||||
prisma.customer.count(),
|
||||
prisma.customer.count({ where: { active: true } }),
|
||||
prisma.equipment.count({ where: { active: true } }),
|
||||
prisma.revision.count({
|
||||
where: {
|
||||
nextDueDate: {
|
||||
gte: today,
|
||||
lte: nextMonth,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.rMA.count(),
|
||||
prisma.rMA.count({ where: { status: { isFinal: false } } }),
|
||||
]);
|
||||
|
||||
successResponse(res, {
|
||||
projects: {
|
||||
total: totalProjects,
|
||||
active: activeProjects,
|
||||
},
|
||||
tasks: {
|
||||
total: totalTasks,
|
||||
pending: pendingTasks,
|
||||
inProgress: inProgressTasks,
|
||||
},
|
||||
customers: {
|
||||
total: totalCustomers,
|
||||
active: activeCustomers,
|
||||
},
|
||||
equipment: {
|
||||
total: totalEquipment,
|
||||
upcomingRevisions,
|
||||
},
|
||||
rma: {
|
||||
total: totalRMAs,
|
||||
pending: pendingRMAs,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní dashboardu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDashboardToday = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const [myTasks, myProjects, recentRMAs] = await Promise.all([
|
||||
// Tasks assigned to me that are not completed
|
||||
prisma.task.findMany({
|
||||
where: {
|
||||
assignees: { some: { userId } },
|
||||
status: { isFinal: false },
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
assignees: { include: { user: { select: { id: true, name: true } } } },
|
||||
},
|
||||
orderBy: [{ priority: { level: 'desc' } }, { deadline: 'asc' }],
|
||||
take: 10,
|
||||
}),
|
||||
|
||||
// My active projects
|
||||
prisma.project.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId: userId },
|
||||
{ members: { some: { userId } } },
|
||||
],
|
||||
status: { isFinal: false },
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
_count: { select: { tasks: true } },
|
||||
},
|
||||
take: 5,
|
||||
}),
|
||||
|
||||
// Recent RMAs
|
||||
prisma.rMA.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ assignedToId: userId },
|
||||
{ createdById: userId },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
}),
|
||||
]);
|
||||
|
||||
successResponse(res, {
|
||||
myTasks,
|
||||
myProjects,
|
||||
recentRMAs,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard today:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní dashboardu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDashboardWeek = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const nextWeek = new Date(today);
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const [tasksDeadlineThisWeek, upcomingRevisions] = await Promise.all([
|
||||
// Tasks with deadline this week
|
||||
prisma.task.findMany({
|
||||
where: {
|
||||
deadline: {
|
||||
gte: today,
|
||||
lte: nextWeek,
|
||||
},
|
||||
status: { isFinal: false },
|
||||
OR: [
|
||||
{ createdById: userId },
|
||||
{ assignees: { some: { userId } } },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
assignees: { include: { user: { select: { id: true, name: true } } } },
|
||||
},
|
||||
orderBy: { deadline: 'asc' },
|
||||
}),
|
||||
|
||||
// Upcoming equipment revisions
|
||||
prisma.revision.findMany({
|
||||
where: {
|
||||
nextDueDate: {
|
||||
gte: today,
|
||||
lte: nextWeek,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
equipment: {
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
type: true,
|
||||
},
|
||||
orderBy: { nextDueDate: 'asc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
successResponse(res, {
|
||||
tasksDeadlineThisWeek,
|
||||
upcomingRevisions,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard week:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní týždenného prehľadu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDashboardStats = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const [
|
||||
totalProjects,
|
||||
activeProjects,
|
||||
totalTasks,
|
||||
completedTasks,
|
||||
totalCustomers,
|
||||
totalEquipment,
|
||||
totalRMAs,
|
||||
openRMAs,
|
||||
] = await Promise.all([
|
||||
prisma.project.count(),
|
||||
prisma.project.count({ where: { status: { isFinal: false } } }),
|
||||
prisma.task.count(),
|
||||
prisma.task.count({ where: { status: { isFinal: true } } }),
|
||||
prisma.customer.count({ where: { active: true } }),
|
||||
prisma.equipment.count({ where: { active: true } }),
|
||||
prisma.rMA.count(),
|
||||
prisma.rMA.count({ where: { status: { isFinal: false } } }),
|
||||
]);
|
||||
|
||||
successResponse(res, {
|
||||
projects: {
|
||||
total: totalProjects,
|
||||
active: activeProjects,
|
||||
},
|
||||
tasks: {
|
||||
total: totalTasks,
|
||||
completed: completedTasks,
|
||||
completionRate: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
|
||||
},
|
||||
customers: {
|
||||
total: totalCustomers,
|
||||
},
|
||||
equipment: {
|
||||
total: totalEquipment,
|
||||
},
|
||||
rmas: {
|
||||
total: totalRMAs,
|
||||
open: openRMAs,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard stats:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní štatistík.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDashboardReminders = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const nextMonth = new Date(today);
|
||||
nextMonth.setDate(nextMonth.getDate() + 30);
|
||||
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const [taskReminders, equipmentRevisions, overdueRMAs] = await Promise.all([
|
||||
// Task reminders
|
||||
prisma.reminder.findMany({
|
||||
where: {
|
||||
userId,
|
||||
dismissed: false,
|
||||
remindAt: {
|
||||
lte: nextMonth,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
task: {
|
||||
include: {
|
||||
status: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { remindAt: 'asc' },
|
||||
}),
|
||||
|
||||
// Equipment revision reminders
|
||||
prisma.revision.findMany({
|
||||
where: {
|
||||
nextDueDate: {
|
||||
gte: today,
|
||||
lte: nextMonth,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
equipment: {
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
type: true,
|
||||
},
|
||||
orderBy: { nextDueDate: 'asc' },
|
||||
take: 10,
|
||||
}),
|
||||
|
||||
// Overdue or old RMAs
|
||||
prisma.rMA.findMany({
|
||||
where: {
|
||||
status: { isFinal: false },
|
||||
createdAt: {
|
||||
lte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000), // Older than 7 days
|
||||
},
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: 10,
|
||||
}),
|
||||
]);
|
||||
|
||||
successResponse(res, {
|
||||
taskReminders,
|
||||
equipmentRevisions,
|
||||
overdueRMAs,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard reminders:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní upomienok.', 500);
|
||||
}
|
||||
};
|
||||
313
backend/src/controllers/equipment.controller.ts
Normal file
313
backend/src/controllers/equipment.controller.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
export const getEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const active = getQueryString(req, 'active');
|
||||
const typeId = getQueryString(req, 'typeId');
|
||||
const customerId = getQueryString(req, 'customerId');
|
||||
|
||||
const where = {
|
||||
...(active !== undefined && { active: active === 'true' }),
|
||||
...(typeId && { typeId }),
|
||||
...(customerId && { customerId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ serialNumber: { contains: search, mode: 'insensitive' as const } },
|
||||
{ address: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [equipment, total] = await Promise.all([
|
||||
prisma.equipment.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
_count: { select: { revisions: true } },
|
||||
},
|
||||
}),
|
||||
prisma.equipment.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, equipment, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zariadení.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEquipmentById = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const equipment = await prisma.equipment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
type: true,
|
||||
customer: true,
|
||||
createdBy: { select: { id: true, name: true, email: true } },
|
||||
revisions: {
|
||||
orderBy: { performedDate: 'desc' },
|
||||
take: 5,
|
||||
include: {
|
||||
type: true,
|
||||
performedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!equipment) {
|
||||
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, equipment);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const equipment = await prisma.equipment.create({
|
||||
data: {
|
||||
name: req.body.name,
|
||||
typeId: req.body.typeId,
|
||||
brand: req.body.brand,
|
||||
model: req.body.model,
|
||||
customerId: req.body.customerId || null,
|
||||
address: req.body.address,
|
||||
location: req.body.location,
|
||||
partNumber: req.body.partNumber,
|
||||
serialNumber: req.body.serialNumber,
|
||||
installDate: req.body.installDate ? new Date(req.body.installDate) : null,
|
||||
warrantyEnd: req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null,
|
||||
warrantyStatus: req.body.warrantyStatus,
|
||||
description: req.body.description,
|
||||
notes: req.body.notes,
|
||||
createdById: req.user!.userId,
|
||||
},
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Equipment', equipment.id, { name: equipment.name });
|
||||
}
|
||||
|
||||
successResponse(res, equipment, 'Zariadenie bolo vytvorené.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating equipment:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const existing = await prisma.equipment.findUnique({ where: { id } });
|
||||
|
||||
if (!existing) {
|
||||
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
const fields = [
|
||||
'name', 'typeId', 'brand', 'model', 'customerId', 'address',
|
||||
'location', 'partNumber', 'serialNumber', 'warrantyStatus',
|
||||
'description', 'notes', 'active'
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
if (req.body[field] !== undefined) {
|
||||
updateData[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.installDate !== undefined) {
|
||||
updateData.installDate = req.body.installDate ? new Date(req.body.installDate) : null;
|
||||
}
|
||||
if (req.body.warrantyEnd !== undefined) {
|
||||
updateData.warrantyEnd = req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null;
|
||||
}
|
||||
|
||||
const equipment = await prisma.equipment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'Equipment', id, updateData);
|
||||
}
|
||||
|
||||
successResponse(res, equipment, 'Zariadenie bolo aktualizované.');
|
||||
} catch (error) {
|
||||
console.error('Error updating equipment:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const equipment = await prisma.equipment.findUnique({ where: { id } });
|
||||
|
||||
if (!equipment) {
|
||||
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await prisma.equipment.update({
|
||||
where: { id },
|
||||
data: { active: false },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'Equipment', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Zariadenie bolo deaktivované.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting equipment:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEquipmentRevisions = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const revisions = await prisma.revision.findMany({
|
||||
where: { equipmentId: id },
|
||||
orderBy: { performedDate: 'desc' },
|
||||
include: {
|
||||
type: true,
|
||||
performedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, revisions);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment revisions:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní revízií.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createEquipmentRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const equipment = await prisma.equipment.findUnique({ where: { id } });
|
||||
if (!equipment) {
|
||||
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const revisionType = await prisma.revisionType.findUnique({
|
||||
where: { id: req.body.typeId },
|
||||
});
|
||||
|
||||
if (!revisionType) {
|
||||
errorResponse(res, 'Typ revízie nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const performedDate = new Date(req.body.performedDate);
|
||||
const nextDueDate = new Date(performedDate);
|
||||
nextDueDate.setDate(nextDueDate.getDate() + revisionType.intervalDays);
|
||||
|
||||
const reminderDate = new Date(nextDueDate);
|
||||
reminderDate.setDate(reminderDate.getDate() - revisionType.reminderDays);
|
||||
|
||||
const revision = await prisma.revision.create({
|
||||
data: {
|
||||
equipmentId: id,
|
||||
typeId: req.body.typeId,
|
||||
performedDate,
|
||||
nextDueDate,
|
||||
reminderDate,
|
||||
performedById: req.user!.userId,
|
||||
findings: req.body.findings,
|
||||
result: req.body.result,
|
||||
notes: req.body.notes,
|
||||
},
|
||||
include: {
|
||||
type: true,
|
||||
performedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Revision', revision.id, {
|
||||
equipmentId: id,
|
||||
type: revisionType.name,
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, revision, 'Revízia bola vytvorená.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating revision:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní revízie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEquipmentReminders = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const days = parseQueryInt(req.query.days, 30);
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + days);
|
||||
|
||||
const revisions = await prisma.revision.findMany({
|
||||
where: {
|
||||
nextDueDate: {
|
||||
gte: new Date(),
|
||||
lte: futureDate,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
equipment: {
|
||||
include: {
|
||||
type: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
type: true,
|
||||
},
|
||||
orderBy: { nextDueDate: 'asc' },
|
||||
});
|
||||
|
||||
successResponse(res, revisions);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment reminders:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní upomienok.', 500);
|
||||
}
|
||||
};
|
||||
288
backend/src/controllers/projects.controller.ts
Normal file
288
backend/src/controllers/projects.controller.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
|
||||
export const getProjects = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const statusId = getQueryString(req, 'statusId');
|
||||
const customerId = getQueryString(req, 'customerId');
|
||||
const ownerId = getQueryString(req, 'ownerId');
|
||||
|
||||
const where = {
|
||||
...(statusId && { statusId }),
|
||||
...(customerId && { customerId }),
|
||||
...(ownerId && { ownerId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ description: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [projects, total] = await Promise.all([
|
||||
prisma.project.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
status: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
owner: { select: { id: true, name: true, email: true } },
|
||||
_count: { select: { tasks: true, members: true } },
|
||||
},
|
||||
}),
|
||||
prisma.project.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, projects, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní projektov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getProject = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
status: true,
|
||||
customer: true,
|
||||
owner: { select: { id: true, name: true, email: true } },
|
||||
members: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
_count: { select: { tasks: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
errorResponse(res, 'Projekt nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, project);
|
||||
} catch (error) {
|
||||
console.error('Error fetching project:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní projektu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createProject = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
let { statusId } = req.body;
|
||||
|
||||
// Get initial status if not provided
|
||||
if (!statusId) {
|
||||
const initialStatus = await configService.getInitialTaskStatus();
|
||||
if (initialStatus) {
|
||||
statusId = initialStatus.id;
|
||||
} else {
|
||||
// Fallback: vezmi prvý aktívny status
|
||||
const allStatuses = await configService.getTaskStatuses();
|
||||
const statuses = allStatuses as { id: string }[];
|
||||
if (statuses.length > 0) {
|
||||
statusId = statuses[0].id;
|
||||
} else {
|
||||
errorResponse(res, 'Žiadny status nie je nakonfigurovaný. Spustite seed.', 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
customerId: req.body.customerId || null,
|
||||
ownerId: req.user!.userId,
|
||||
statusId,
|
||||
softDeadline: req.body.softDeadline ? new Date(req.body.softDeadline) : null,
|
||||
hardDeadline: req.body.hardDeadline ? new Date(req.body.hardDeadline) : null,
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
owner: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Project', project.id, { name: project.name });
|
||||
}
|
||||
|
||||
successResponse(res, project, 'Projekt bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní projektu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateProject = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const existing = await prisma.project.findUnique({ where: { id } });
|
||||
|
||||
if (!existing) {
|
||||
errorResponse(res, 'Projekt nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (req.body.name) updateData.name = req.body.name;
|
||||
if (req.body.description !== undefined) updateData.description = req.body.description;
|
||||
if (req.body.customerId !== undefined) updateData.customerId = req.body.customerId || null;
|
||||
if (req.body.statusId) updateData.statusId = req.body.statusId;
|
||||
if (req.body.softDeadline !== undefined) {
|
||||
updateData.softDeadline = req.body.softDeadline ? new Date(req.body.softDeadline) : null;
|
||||
}
|
||||
if (req.body.hardDeadline !== undefined) {
|
||||
updateData.hardDeadline = req.body.hardDeadline ? new Date(req.body.hardDeadline) : null;
|
||||
}
|
||||
|
||||
const project = await prisma.project.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
status: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
owner: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'Project', id, updateData);
|
||||
}
|
||||
|
||||
successResponse(res, project, 'Projekt bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating project:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii projektu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteProject = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const project = await prisma.project.findUnique({ where: { id } });
|
||||
|
||||
if (!project) {
|
||||
errorResponse(res, 'Projekt nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.project.delete({ where: { id } });
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'Project', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Projekt bol vymazaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní projektu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateProjectStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { statusId } = req.body;
|
||||
|
||||
const project = await prisma.project.update({
|
||||
where: { id },
|
||||
data: { statusId },
|
||||
include: { status: true },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('STATUS_CHANGE', 'Project', id, { statusId });
|
||||
}
|
||||
|
||||
successResponse(res, project, 'Status projektu bol zmenený.');
|
||||
} catch (error) {
|
||||
console.error('Error updating project status:', error);
|
||||
errorResponse(res, 'Chyba pri zmene statusu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getProjectTasks = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: { projectId: id },
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
assignees: {
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
},
|
||||
},
|
||||
orderBy: [{ priority: { level: 'desc' } }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
successResponse(res, tasks);
|
||||
} catch (error) {
|
||||
console.error('Error fetching project tasks:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní úloh.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const addProjectMember = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { userId } = req.body;
|
||||
|
||||
const member = await prisma.projectMember.create({
|
||||
data: {
|
||||
projectId: id,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, member, 'Člen bol pridaný.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error adding project member:', error);
|
||||
errorResponse(res, 'Chyba pri pridávaní člena.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeProjectMember = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const userId = getParam(req, 'userId');
|
||||
|
||||
await prisma.projectMember.delete({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
projectId: id,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, null, 'Člen bol odstránený.');
|
||||
} catch (error) {
|
||||
console.error('Error removing project member:', error);
|
||||
errorResponse(res, 'Chyba pri odstraňovaní člena.', 500);
|
||||
}
|
||||
};
|
||||
414
backend/src/controllers/rma.controller.ts
Normal file
414
backend/src/controllers/rma.controller.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
|
||||
async function generateRMANumber(): Promise<string> {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
|
||||
const startOfDay = new Date(today.setHours(0, 0, 0, 0));
|
||||
const endOfDay = new Date(today.setHours(23, 59, 59, 999));
|
||||
|
||||
const count = await prisma.rMA.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfDay,
|
||||
lte: endOfDay,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sequence = String(count + 1).padStart(3, '0');
|
||||
return `RMA-${year}${month}${day}${sequence}`;
|
||||
}
|
||||
|
||||
export const getRMAs = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const statusId = getQueryString(req, 'statusId');
|
||||
const customerId = getQueryString(req, 'customerId');
|
||||
const assignedToId = getQueryString(req, 'assignedToId');
|
||||
|
||||
const where = {
|
||||
...(statusId && { statusId }),
|
||||
...(customerId && { customerId }),
|
||||
...(assignedToId && { assignedToId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ rmaNumber: { contains: search, mode: 'insensitive' as const } },
|
||||
{ productName: { contains: search, mode: 'insensitive' as const } },
|
||||
{ customerName: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [rmas, total] = await Promise.all([
|
||||
prisma.rMA.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
status: true,
|
||||
proposedSolution: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
assignedTo: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
}),
|
||||
prisma.rMA.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, rmas, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching RMAs:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní reklamácií.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRMA = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const rma = await prisma.rMA.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
status: true,
|
||||
proposedSolution: true,
|
||||
customer: true,
|
||||
assignedTo: { select: { id: true, name: true, email: true } },
|
||||
createdBy: { select: { id: true, name: true, email: true } },
|
||||
approvedBy: { select: { id: true, name: true } },
|
||||
attachments: {
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
},
|
||||
statusHistory: {
|
||||
orderBy: { changedAt: 'desc' },
|
||||
include: {
|
||||
changedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!rma) {
|
||||
errorResponse(res, 'Reklamácia nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, rma);
|
||||
} catch (error) {
|
||||
console.error('Error fetching RMA:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRMA = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
let { statusId, proposedSolutionId } = req.body;
|
||||
|
||||
// Get initial status if not provided
|
||||
if (!statusId) {
|
||||
const initialStatus = await configService.getInitialRMAStatus();
|
||||
if (initialStatus) {
|
||||
statusId = initialStatus.id;
|
||||
} else {
|
||||
errorResponse(res, 'Predvolený status reklamácie nie je nakonfigurovaný.', 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get default solution (Assessment) if not provided
|
||||
if (!proposedSolutionId) {
|
||||
const solutions = await configService.getRMASolutions();
|
||||
const defaultSolution = (solutions as { id: string; code: string }[]).find(
|
||||
(s) => s.code === 'ASSESSMENT'
|
||||
);
|
||||
if (defaultSolution) {
|
||||
proposedSolutionId = defaultSolution.id;
|
||||
}
|
||||
}
|
||||
|
||||
const rmaNumber = await generateRMANumber();
|
||||
|
||||
// Check if customer role requires approval
|
||||
let requiresApproval = false;
|
||||
if (req.user?.roleCode === 'CUSTOMER') {
|
||||
const setting = await configService.getSetting<boolean>('RMA_CUSTOMER_REQUIRES_APPROVAL');
|
||||
requiresApproval = setting === true;
|
||||
}
|
||||
|
||||
const rma = await prisma.rMA.create({
|
||||
data: {
|
||||
rmaNumber,
|
||||
customerId: req.body.customerId || null,
|
||||
customerName: req.body.customerName,
|
||||
customerAddress: req.body.customerAddress,
|
||||
customerEmail: req.body.customerEmail,
|
||||
customerPhone: req.body.customerPhone,
|
||||
customerICO: req.body.customerICO,
|
||||
submittedBy: req.body.submittedBy,
|
||||
productName: req.body.productName,
|
||||
invoiceNumber: req.body.invoiceNumber,
|
||||
purchaseDate: req.body.purchaseDate ? new Date(req.body.purchaseDate) : null,
|
||||
productNumber: req.body.productNumber,
|
||||
serialNumber: req.body.serialNumber,
|
||||
accessories: req.body.accessories,
|
||||
issueDescription: req.body.issueDescription,
|
||||
statusId,
|
||||
proposedSolutionId,
|
||||
requiresApproval,
|
||||
createdById: req.user!.userId,
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
proposedSolution: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Create initial status history
|
||||
await prisma.rMAStatusHistory.create({
|
||||
data: {
|
||||
rmaId: rma.id,
|
||||
toStatusId: statusId,
|
||||
changedById: req.user!.userId,
|
||||
notes: 'Reklamácia vytvorená',
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'RMA', rma.id, { rmaNumber });
|
||||
}
|
||||
|
||||
successResponse(res, rma, 'Reklamácia bola vytvorená.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating RMA:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRMA = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const existing = await prisma.rMA.findUnique({ where: { id } });
|
||||
|
||||
if (!existing) {
|
||||
errorResponse(res, 'Reklamácia nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
const fields = [
|
||||
'customerId', 'customerName', 'customerAddress', 'customerEmail',
|
||||
'customerPhone', 'customerICO', 'submittedBy', 'productName',
|
||||
'invoiceNumber', 'productNumber', 'serialNumber', 'accessories',
|
||||
'issueDescription', 'proposedSolutionId', 'receivedLocation',
|
||||
'internalNotes', 'resolutionNotes', 'assignedToId'
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
if (req.body[field] !== undefined) {
|
||||
updateData[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.purchaseDate !== undefined) {
|
||||
updateData.purchaseDate = req.body.purchaseDate ? new Date(req.body.purchaseDate) : null;
|
||||
}
|
||||
if (req.body.receivedDate !== undefined) {
|
||||
updateData.receivedDate = req.body.receivedDate ? new Date(req.body.receivedDate) : null;
|
||||
}
|
||||
if (req.body.resolutionDate !== undefined) {
|
||||
updateData.resolutionDate = req.body.resolutionDate ? new Date(req.body.resolutionDate) : null;
|
||||
}
|
||||
|
||||
const rma = await prisma.rMA.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
status: true,
|
||||
proposedSolution: true,
|
||||
customer: { select: { id: true, name: true } },
|
||||
assignedTo: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'RMA', id, updateData);
|
||||
}
|
||||
|
||||
successResponse(res, rma, 'Reklamácia bola aktualizovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error updating RMA:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRMA = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const rma = await prisma.rMA.findUnique({ where: { id } });
|
||||
|
||||
if (!rma) {
|
||||
errorResponse(res, 'Reklamácia nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.rMA.delete({ where: { id } });
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'RMA', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Reklamácia bola vymazaná.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting RMA:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRMAStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { statusId, notes } = req.body;
|
||||
|
||||
const rma = await prisma.rMA.findUnique({
|
||||
where: { id },
|
||||
include: { status: true },
|
||||
});
|
||||
|
||||
if (!rma) {
|
||||
errorResponse(res, 'Reklamácia nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatus = await prisma.rMAStatus.findUnique({ where: { id: statusId } });
|
||||
|
||||
if (!newStatus) {
|
||||
errorResponse(res, 'Status nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate transition (basic - can be enhanced with workflow rules)
|
||||
const currentStatus = rma.status;
|
||||
if (currentStatus.canTransitionTo) {
|
||||
const allowedTransitions = currentStatus.canTransitionTo as string[];
|
||||
if (!allowedTransitions.includes(newStatus.code)) {
|
||||
errorResponse(
|
||||
res,
|
||||
`Prechod zo statusu "${currentStatus.name}" na "${newStatus.name}" nie je povolený.`,
|
||||
400
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedRMA = await prisma.rMA.update({
|
||||
where: { id },
|
||||
data: {
|
||||
statusId,
|
||||
closedAt: newStatus.isFinal ? new Date() : null,
|
||||
},
|
||||
include: { status: true },
|
||||
});
|
||||
|
||||
// Create status history
|
||||
await prisma.rMAStatusHistory.create({
|
||||
data: {
|
||||
rmaId: id,
|
||||
fromStatusId: rma.statusId,
|
||||
toStatusId: statusId,
|
||||
changedById: req.user!.userId,
|
||||
notes,
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('STATUS_CHANGE', 'RMA', id, {
|
||||
from: currentStatus.code,
|
||||
to: newStatus.code,
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, updatedRMA, 'Status reklamácie bol zmenený.');
|
||||
} catch (error) {
|
||||
console.error('Error updating RMA status:', error);
|
||||
errorResponse(res, 'Chyba pri zmene statusu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const approveRMA = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const rma = await prisma.rMA.update({
|
||||
where: { id },
|
||||
data: {
|
||||
requiresApproval: false,
|
||||
approvedById: req.user!.userId,
|
||||
approvedAt: new Date(),
|
||||
},
|
||||
include: { status: true },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'RMA', id, { approved: true });
|
||||
}
|
||||
|
||||
successResponse(res, rma, 'Reklamácia bola schválená.');
|
||||
} catch (error) {
|
||||
console.error('Error approving RMA:', error);
|
||||
errorResponse(res, 'Chyba pri schvaľovaní reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const addRMAComment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { content } = req.body;
|
||||
|
||||
const comment = await prisma.rMAComment.create({
|
||||
data: {
|
||||
rmaId: id,
|
||||
userId: req.user!.userId,
|
||||
content,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, comment, 'Komentár bol pridaný.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error adding RMA comment:', error);
|
||||
errorResponse(res, 'Chyba pri pridávaní komentára.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateRMANumberEndpoint = async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const rmaNumber = await generateRMANumber();
|
||||
successResponse(res, { rmaNumber });
|
||||
} catch (error) {
|
||||
console.error('Error generating RMA number:', error);
|
||||
errorResponse(res, 'Chyba pri generovaní RMA čísla.', 500);
|
||||
}
|
||||
};
|
||||
450
backend/src/controllers/settings.controller.ts
Normal file
450
backend/src/controllers/settings.controller.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
|
||||
// ==================== EQUIPMENT TYPES ====================
|
||||
|
||||
export const getEquipmentTypes = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const types = await configService.getEquipmentTypes(activeOnly);
|
||||
successResponse(res, types);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment types:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní typov zariadení.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createEquipmentType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const type = await prisma.equipmentType.create({ data: req.body });
|
||||
configService.clearCacheKey('equipment_types');
|
||||
successResponse(res, type, 'Typ zariadenia bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating equipment type:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní typu zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEquipmentType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const type = await prisma.equipmentType.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('equipment_types');
|
||||
successResponse(res, type, 'Typ zariadenia bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating equipment type:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii typu zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEquipmentType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.equipmentType.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('equipment_types');
|
||||
successResponse(res, null, 'Typ zariadenia bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting equipment type:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní typu zariadenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== REVISION TYPES ====================
|
||||
|
||||
export const getRevisionTypes = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const types = await configService.getRevisionTypes(activeOnly);
|
||||
successResponse(res, types);
|
||||
} catch (error) {
|
||||
console.error('Error fetching revision types:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní typov revízií.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRevisionType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const type = await prisma.revisionType.create({ data: req.body });
|
||||
configService.clearCacheKey('revision_types');
|
||||
successResponse(res, type, 'Typ revízie bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating revision type:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní typu revízie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRevisionType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const type = await prisma.revisionType.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('revision_types');
|
||||
successResponse(res, type, 'Typ revízie bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating revision type:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii typu revízie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRevisionType = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.revisionType.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('revision_types');
|
||||
successResponse(res, null, 'Typ revízie bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting revision type:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní typu revízie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== RMA STATUSES ====================
|
||||
|
||||
export const getRMAStatuses = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const statuses = await configService.getRMAStatuses(activeOnly);
|
||||
successResponse(res, statuses);
|
||||
} catch (error) {
|
||||
console.error('Error fetching RMA statuses:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní statusov reklamácií.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRMAStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = await prisma.rMAStatus.create({ data: req.body });
|
||||
configService.clearCacheKey('rma_statuses');
|
||||
successResponse(res, status, 'Status reklamácie bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating RMA status:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní statusu reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRMAStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const status = await prisma.rMAStatus.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('rma_statuses');
|
||||
successResponse(res, status, 'Status reklamácie bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating RMA status:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii statusu reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRMAStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.rMAStatus.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('rma_statuses');
|
||||
successResponse(res, null, 'Status reklamácie bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting RMA status:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní statusu reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== RMA SOLUTIONS ====================
|
||||
|
||||
export const getRMASolutions = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const solutions = await configService.getRMASolutions(activeOnly);
|
||||
successResponse(res, solutions);
|
||||
} catch (error) {
|
||||
console.error('Error fetching RMA solutions:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní riešení reklamácií.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRMASolution = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const solution = await prisma.rMASolution.create({ data: req.body });
|
||||
configService.clearCacheKey('rma_solutions');
|
||||
successResponse(res, solution, 'Riešenie reklamácie bolo vytvorené.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating RMA solution:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní riešenia reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRMASolution = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const solution = await prisma.rMASolution.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('rma_solutions');
|
||||
successResponse(res, solution, 'Riešenie reklamácie bolo aktualizované.');
|
||||
} catch (error) {
|
||||
console.error('Error updating RMA solution:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii riešenia reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRMASolution = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.rMASolution.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('rma_solutions');
|
||||
successResponse(res, null, 'Riešenie reklamácie bolo deaktivované.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting RMA solution:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní riešenia reklamácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== TASK STATUSES ====================
|
||||
|
||||
export const getTaskStatuses = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const statuses = await configService.getTaskStatuses(activeOnly);
|
||||
successResponse(res, statuses);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task statuses:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní statusov úloh.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTaskStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = await prisma.taskStatus.create({ data: req.body });
|
||||
configService.clearCacheKey('task_statuses');
|
||||
successResponse(res, status, 'Status úlohy bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating task status:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní statusu úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTaskStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const status = await prisma.taskStatus.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('task_statuses');
|
||||
successResponse(res, status, 'Status úlohy bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating task status:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii statusu úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTaskStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.taskStatus.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('task_statuses');
|
||||
successResponse(res, null, 'Status úlohy bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting task status:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní statusu úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== PRIORITIES ====================
|
||||
|
||||
export const getPriorities = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const priorities = await configService.getPriorities(activeOnly);
|
||||
successResponse(res, priorities);
|
||||
} catch (error) {
|
||||
console.error('Error fetching priorities:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní priorít.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createPriority = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const priority = await prisma.priority.create({ data: req.body });
|
||||
configService.clearCacheKey('priorities');
|
||||
successResponse(res, priority, 'Priorita bola vytvorená.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating priority:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní priority.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePriority = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const priority = await prisma.priority.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('priorities');
|
||||
successResponse(res, priority, 'Priorita bola aktualizovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error updating priority:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii priority.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePriority = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.priority.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('priorities');
|
||||
successResponse(res, null, 'Priorita bola deaktivovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting priority:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní priority.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== TAGS ====================
|
||||
|
||||
export const getTags = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const entityType = getQueryString(req, 'entityType');
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const tags = await configService.getTags(entityType, activeOnly);
|
||||
successResponse(res, tags);
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní tagov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTag = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tag = await prisma.tag.create({ data: req.body });
|
||||
configService.clearCacheKey('tags');
|
||||
successResponse(res, tag, 'Tag bol vytvorený.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating tag:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní tagu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTag = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const tag = await prisma.tag.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('tags');
|
||||
successResponse(res, tag, 'Tag bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating tag:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii tagu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTag = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.tag.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('tags');
|
||||
successResponse(res, null, 'Tag bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting tag:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní tagu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== USER ROLES ====================
|
||||
|
||||
export const getUserRoles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const activeOnly = getQueryString(req, 'activeOnly') !== 'false';
|
||||
const roles = await configService.getUserRoles(activeOnly);
|
||||
successResponse(res, roles);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user roles:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní rolí.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createUserRole = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const role = await prisma.userRole.create({ data: req.body });
|
||||
configService.clearCacheKey('user_roles');
|
||||
successResponse(res, role, 'Rola bola vytvorená.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating user role:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní roly.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRole = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const role = await prisma.userRole.update({ where: { id }, data: req.body });
|
||||
configService.clearCacheKey('user_roles');
|
||||
successResponse(res, role, 'Rola bola aktualizovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error updating user role:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii roly.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserRole = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
await prisma.userRole.update({ where: { id }, data: { active: false } });
|
||||
configService.clearCacheKey('user_roles');
|
||||
successResponse(res, null, 'Rola bola deaktivovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting user role:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní roly.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== SYSTEM SETTINGS ====================
|
||||
|
||||
export const getSystemSettings = async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const settings = await prisma.systemSetting.findMany({
|
||||
orderBy: [{ category: 'asc' }, { key: 'asc' }],
|
||||
});
|
||||
successResponse(res, settings);
|
||||
} catch (error) {
|
||||
console.error('Error fetching system settings:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní nastavení.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSetting = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const key = getParam(req, 'key');
|
||||
const setting = await prisma.systemSetting.findUnique({ where: { key } });
|
||||
|
||||
if (!setting) {
|
||||
errorResponse(res, 'Nastavenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, setting);
|
||||
} catch (error) {
|
||||
console.error('Error fetching system setting:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní nastavenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSystemSetting = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const key = getParam(req, 'key');
|
||||
const { value } = req.body;
|
||||
|
||||
const setting = await prisma.systemSetting.update({
|
||||
where: { key },
|
||||
data: { value },
|
||||
});
|
||||
|
||||
configService.clearCacheKey(`setting_${key}`);
|
||||
successResponse(res, setting, 'Nastavenie bolo aktualizované.');
|
||||
} catch (error) {
|
||||
console.error('Error updating system setting:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii nastavenia.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSettingsByCategory = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const category = getParam(req, 'category');
|
||||
const settings = await configService.getSettingsByCategory(category);
|
||||
successResponse(res, settings);
|
||||
} catch (error) {
|
||||
console.error('Error fetching system settings by category:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní nastavení.', 500);
|
||||
}
|
||||
};
|
||||
400
backend/src/controllers/tasks.controller.ts
Normal file
400
backend/src/controllers/tasks.controller.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
|
||||
export const getTasks = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const projectId = getQueryString(req, 'projectId');
|
||||
const statusId = getQueryString(req, 'statusId');
|
||||
const priorityId = getQueryString(req, 'priorityId');
|
||||
const createdById = getQueryString(req, 'createdById');
|
||||
const assigneeId = getQueryString(req, 'assigneeId');
|
||||
|
||||
const where = {
|
||||
...(projectId && { projectId }),
|
||||
...(statusId && { statusId }),
|
||||
...(priorityId && { priorityId }),
|
||||
...(createdById && { createdById }),
|
||||
...(assigneeId && {
|
||||
assignees: { some: { userId: assigneeId } },
|
||||
}),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' as const } },
|
||||
{ description: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [tasks, total] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ priority: { level: 'desc' } }, { createdAt: 'desc' }],
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
assignees: {
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
},
|
||||
_count: { select: { subTasks: true, comments: true } },
|
||||
},
|
||||
}),
|
||||
prisma.task.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, tasks, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching tasks:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní úloh.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTask = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
parent: { select: { id: true, title: true } },
|
||||
subTasks: {
|
||||
include: {
|
||||
status: true,
|
||||
assignees: { include: { user: { select: { id: true, name: true } } } },
|
||||
},
|
||||
},
|
||||
createdBy: { select: { id: true, name: true, email: true } },
|
||||
assignees: {
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
},
|
||||
comments: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
errorResponse(res, 'Úloha nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, task);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTask = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
let { statusId, priorityId } = req.body;
|
||||
|
||||
// Get defaults if not provided
|
||||
if (!statusId) {
|
||||
const initialStatus = await configService.getInitialTaskStatus();
|
||||
if (initialStatus) {
|
||||
statusId = initialStatus.id;
|
||||
} else {
|
||||
// Fallback: vezmi prvý aktívny status
|
||||
const allStatuses = await configService.getTaskStatuses();
|
||||
const statuses = allStatuses as { id: string }[];
|
||||
if (statuses.length > 0) {
|
||||
statusId = statuses[0].id;
|
||||
} else {
|
||||
errorResponse(res, 'Žiadny status nie je nakonfigurovaný. Spustite seed.', 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!priorityId) {
|
||||
const defaultPriority = await configService.getDefaultPriority();
|
||||
if (defaultPriority) {
|
||||
priorityId = defaultPriority.id;
|
||||
} else {
|
||||
// Fallback: vezmi prvú aktívnu prioritu
|
||||
const allPriorities = await configService.getPriorities();
|
||||
const priorities = allPriorities as { id: string }[];
|
||||
if (priorities.length > 0) {
|
||||
priorityId = priorities[0].id;
|
||||
} else {
|
||||
errorResponse(res, 'Žiadna priorita nie je nakonfigurovaná. Spustite seed.', 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
title: req.body.title,
|
||||
description: req.body.description,
|
||||
projectId: req.body.projectId || null,
|
||||
parentId: req.body.parentId || null,
|
||||
statusId,
|
||||
priorityId,
|
||||
deadline: req.body.deadline ? new Date(req.body.deadline) : null,
|
||||
createdById: req.user!.userId,
|
||||
},
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
createdBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Add assignees if provided
|
||||
if (req.body.assigneeIds && req.body.assigneeIds.length > 0) {
|
||||
await prisma.taskAssignee.createMany({
|
||||
data: req.body.assigneeIds.map((userId: string) => ({
|
||||
taskId: task.id,
|
||||
userId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Task', task.id, { title: task.title });
|
||||
}
|
||||
|
||||
successResponse(res, task, 'Úloha bola vytvorená.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
errorResponse(res, 'Chyba pri vytváraní úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTask = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const existing = await prisma.task.findUnique({ where: { id } });
|
||||
|
||||
if (!existing) {
|
||||
errorResponse(res, 'Úloha nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (req.body.title) updateData.title = req.body.title;
|
||||
if (req.body.description !== undefined) updateData.description = req.body.description;
|
||||
if (req.body.projectId !== undefined) updateData.projectId = req.body.projectId || null;
|
||||
if (req.body.parentId !== undefined) updateData.parentId = req.body.parentId || null;
|
||||
if (req.body.statusId) updateData.statusId = req.body.statusId;
|
||||
if (req.body.priorityId) updateData.priorityId = req.body.priorityId;
|
||||
if (req.body.deadline !== undefined) {
|
||||
updateData.deadline = req.body.deadline ? new Date(req.body.deadline) : null;
|
||||
}
|
||||
|
||||
const task = await prisma.task.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
assignees: { include: { user: { select: { id: true, name: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
// Update assignees if provided
|
||||
if (req.body.assigneeIds !== undefined) {
|
||||
// Remove all current assignees
|
||||
await prisma.taskAssignee.deleteMany({ where: { taskId: id } });
|
||||
|
||||
// Add new assignees
|
||||
if (req.body.assigneeIds.length > 0) {
|
||||
await prisma.taskAssignee.createMany({
|
||||
data: req.body.assigneeIds.map((userId: string) => ({
|
||||
taskId: id,
|
||||
userId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch task with updated assignees
|
||||
const updatedTask = await prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
status: true,
|
||||
priority: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
assignees: { include: { user: { select: { id: true, name: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'Task', id, updateData);
|
||||
}
|
||||
|
||||
successResponse(res, updatedTask, 'Úloha bola aktualizovaná.');
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTask = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const task = await prisma.task.findUnique({ where: { id } });
|
||||
|
||||
if (!task) {
|
||||
errorResponse(res, 'Úloha nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.task.delete({ where: { id } });
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'Task', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Úloha bola vymazaná.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting task:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní úlohy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTaskStatus = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { statusId } = req.body;
|
||||
|
||||
const status = await prisma.taskStatus.findUnique({ where: { id: statusId } });
|
||||
|
||||
const task = await prisma.task.update({
|
||||
where: { id },
|
||||
data: {
|
||||
statusId,
|
||||
completedAt: status?.isFinal ? new Date() : null,
|
||||
},
|
||||
include: { status: true },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('STATUS_CHANGE', 'Task', id, { statusId });
|
||||
}
|
||||
|
||||
successResponse(res, task, 'Status úlohy bol zmenený.');
|
||||
} catch (error) {
|
||||
console.error('Error updating task status:', error);
|
||||
errorResponse(res, 'Chyba pri zmene statusu.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const addTaskAssignee = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { userId } = req.body;
|
||||
|
||||
const assignee = await prisma.taskAssignee.create({
|
||||
data: { taskId: id, userId },
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
|
||||
successResponse(res, assignee, 'Priraďovateľ bol pridaný.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error adding task assignee:', error);
|
||||
errorResponse(res, 'Chyba pri pridávaní priraďovateľa.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeTaskAssignee = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const userId = getParam(req, 'userId');
|
||||
|
||||
await prisma.taskAssignee.delete({
|
||||
where: { taskId_userId: { taskId: id, userId } },
|
||||
});
|
||||
|
||||
successResponse(res, null, 'Priraďovateľ bol odstránený.');
|
||||
} catch (error) {
|
||||
console.error('Error removing task assignee:', error);
|
||||
errorResponse(res, 'Chyba pri odstraňovaní priraďovateľa.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTaskComments = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const comments = await prisma.comment.findMany({
|
||||
where: { taskId: id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, comments);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task comments:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní komentárov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const addTaskComment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { content } = req.body;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// Načítať úlohu s assignees pre kontrolu oprávnení
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
assignees: { select: { userId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
errorResponse(res, 'Úloha nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kontrola oprávnení - len autor alebo priradený môže komentovať
|
||||
const isCreator = task.createdById === userId;
|
||||
const isAssignee = task.assignees.some(a => a.userId === userId);
|
||||
|
||||
if (!isCreator && !isAssignee) {
|
||||
errorResponse(res, 'Nemáte oprávnenie komentovať túto úlohu.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = await prisma.comment.create({
|
||||
data: {
|
||||
taskId: id,
|
||||
userId,
|
||||
content,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, comment, 'Komentár bol pridaný.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error adding task comment:', error);
|
||||
errorResponse(res, 'Chyba pri pridávaní komentára.', 500);
|
||||
}
|
||||
};
|
||||
272
backend/src/controllers/users.controller.ts
Normal file
272
backend/src/controllers/users.controller.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { Response } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
// Jednoduchý zoznam aktívnych používateľov (pre select/dropdown)
|
||||
// Podporuje server-side vyhľadávanie pre lepší výkon pri veľkom počte používateľov
|
||||
export const getUsersSimple = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const search = getQueryString(req, 'search');
|
||||
const limit = parseQueryInt(req.query.limit, 50); // Default 50, max 100
|
||||
const actualLimit = Math.min(limit, 100);
|
||||
|
||||
const where = {
|
||||
active: true,
|
||||
...(search && {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ email: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where,
|
||||
take: actualLimit,
|
||||
orderBy: { name: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, users);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users simple:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní používateľov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsers = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const page = parseQueryInt(req.query.page, 1);
|
||||
const limit = parseQueryInt(req.query.limit, 20);
|
||||
const skip = (page - 1) * limit;
|
||||
const search = getQueryString(req, 'search');
|
||||
const active = getQueryString(req, 'active');
|
||||
const roleId = getQueryString(req, 'roleId');
|
||||
|
||||
const where = {
|
||||
...(active !== undefined && { active: active === 'true' }),
|
||||
...(roleId && { roleId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ email: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
active: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
paginatedResponse(res, users, total, page, limit);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní používateľov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUser = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
active: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
errorResponse(res, 'Používateľ nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
successResponse(res, user);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní používateľa.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUser = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { email, name, active, password } = req.body;
|
||||
|
||||
const existingUser = await prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!existingUser) {
|
||||
errorResponse(res, 'Používateľ nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check email uniqueness if changing
|
||||
if (email && email !== existingUser.email) {
|
||||
const emailExists = await prisma.user.findUnique({ where: { email } });
|
||||
if (emailExists) {
|
||||
errorResponse(res, 'Email je už používaný.', 409);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (email) updateData.email = email;
|
||||
if (name) updateData.name = name;
|
||||
if (active !== undefined) updateData.active = active;
|
||||
if (password) updateData.password = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
active: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'User', id, updateData);
|
||||
}
|
||||
|
||||
successResponse(res, user, 'Používateľ bol aktualizovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
errorResponse(res, 'Chyba pri aktualizácii používateľa.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUser = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
|
||||
// Prevent self-deletion
|
||||
if (req.user?.userId === id) {
|
||||
errorResponse(res, 'Nemôžete vymazať vlastný účet.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
errorResponse(res, 'Používateľ nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft delete - just deactivate
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { active: false },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE', 'User', id);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Používateľ bol deaktivovaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní používateľa.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRole = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const id = getParam(req, 'id');
|
||||
const { roleId } = req.body;
|
||||
|
||||
// Prevent changing own role
|
||||
if (req.user?.userId === id) {
|
||||
errorResponse(res, 'Nemôžete zmeniť vlastnú rolu.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
errorResponse(res, 'Používateľ nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const role = await prisma.userRole.findUnique({ where: { id: roleId } });
|
||||
|
||||
if (!role) {
|
||||
errorResponse(res, 'Rola neexistuje.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { roleId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPDATE', 'User', id, { roleId, roleName: role.name });
|
||||
}
|
||||
|
||||
successResponse(res, updatedUser, 'Rola používateľa bola zmenená.');
|
||||
} catch (error) {
|
||||
console.error('Error updating user role:', error);
|
||||
errorResponse(res, 'Chyba pri zmene roly.', 500);
|
||||
}
|
||||
};
|
||||
72
backend/src/index.ts
Normal file
72
backend/src/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import { env } from './config/env';
|
||||
import routes from './routes';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
import prisma from './config/database';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: env.FRONTEND_URL,
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Request parsing
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Logging
|
||||
if (env.isDev) {
|
||||
app.use(morgan('dev'));
|
||||
} else {
|
||||
app.use(morgan('combined'));
|
||||
}
|
||||
|
||||
// Health check
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/api', routes);
|
||||
|
||||
// Error handlers
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Start server
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// Test database connection
|
||||
await prisma.$connect();
|
||||
console.log('Database connected successfully');
|
||||
|
||||
app.listen(env.PORT, () => {
|
||||
console.log(`Server running on http://localhost:${env.PORT}`);
|
||||
console.log(`Environment: ${env.NODE_ENV}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM received, shutting down...');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('SIGINT received, shutting down...');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
startServer();
|
||||
73
backend/src/middleware/activityLog.middleware.ts
Normal file
73
backend/src/middleware/activityLog.middleware.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import { AuthRequest } from './auth.middleware';
|
||||
import prisma from '../config/database';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
type ActionType = 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE' | 'LOGIN' | 'LOGOUT';
|
||||
type EntityType = 'User' | 'Project' | 'Task' | 'Customer' | 'Equipment' | 'Revision' | 'RMA';
|
||||
|
||||
export const logActivity = async (
|
||||
userId: string,
|
||||
action: ActionType,
|
||||
entity: EntityType,
|
||||
entityId: string,
|
||||
changes?: Record<string, unknown>,
|
||||
ipAddress?: string,
|
||||
userAgent?: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await prisma.activityLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action,
|
||||
entity,
|
||||
entityId,
|
||||
changes: changes as Prisma.InputJsonValue,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to log activity:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Middleware to attach logging helper to request
|
||||
export const activityLogger = (
|
||||
req: AuthRequest,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
req.logActivity = async (
|
||||
action: ActionType,
|
||||
entity: EntityType,
|
||||
entityId: string,
|
||||
changes?: Record<string, unknown>
|
||||
) => {
|
||||
if (req.user) {
|
||||
await logActivity(
|
||||
req.user.userId,
|
||||
action,
|
||||
entity,
|
||||
entityId,
|
||||
changes,
|
||||
req.ip,
|
||||
req.get('User-Agent')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Extend Express Request type
|
||||
declare module 'express' {
|
||||
interface Request {
|
||||
logActivity?: (
|
||||
action: ActionType,
|
||||
entity: EntityType,
|
||||
entityId: string,
|
||||
changes?: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
}
|
||||
}
|
||||
100
backend/src/middleware/auth.middleware.ts
Normal file
100
backend/src/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyAccessToken, TokenPayload } from '../config/jwt';
|
||||
import { errorResponse } from '../utils/helpers';
|
||||
import prisma from '../config/database';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: TokenPayload & {
|
||||
permissions: Record<string, string[]>;
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
errorResponse(res, 'Prístup zamietnutý. Token nebol poskytnutý.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = verifyAccessToken(token);
|
||||
|
||||
// Get user role with permissions
|
||||
const userRole = await prisma.userRole.findUnique({
|
||||
where: { id: decoded.roleId },
|
||||
});
|
||||
|
||||
if (!userRole || !userRole.active) {
|
||||
errorResponse(res, 'Rola používateľa nie je aktívna.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user still exists and is active
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
});
|
||||
|
||||
if (!user || !user.active) {
|
||||
errorResponse(res, 'Používateľ neexistuje alebo je neaktívny.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
req.user = {
|
||||
...decoded,
|
||||
permissions: userRole.permissions as Record<string, string[]>,
|
||||
};
|
||||
|
||||
next();
|
||||
} catch {
|
||||
errorResponse(res, 'Neplatný alebo expirovaný token.', 401);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth middleware error:', error);
|
||||
errorResponse(res, 'Chyba autentifikácie.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const optionalAuth = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = verifyAccessToken(token);
|
||||
const userRole = await prisma.userRole.findUnique({
|
||||
where: { id: decoded.roleId },
|
||||
});
|
||||
|
||||
if (userRole && userRole.active) {
|
||||
req.user = {
|
||||
...decoded,
|
||||
permissions: userRole.permissions as Record<string, string[]>,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Token invalid, continue without user
|
||||
}
|
||||
|
||||
next();
|
||||
} catch {
|
||||
next();
|
||||
}
|
||||
};
|
||||
49
backend/src/middleware/errorHandler.ts
Normal file
49
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { env } from '../config/env';
|
||||
|
||||
export interface AppError extends Error {
|
||||
statusCode?: number;
|
||||
isOperational?: boolean;
|
||||
}
|
||||
|
||||
export const errorHandler = (
|
||||
err: AppError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
_next: NextFunction
|
||||
): void => {
|
||||
const statusCode = err.statusCode || 500;
|
||||
const message = err.message || 'Interná chyba servera';
|
||||
|
||||
console.error(`[ERROR] ${statusCode} - ${message}`);
|
||||
console.error(err.stack);
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
...(env.isDev && {
|
||||
stack: err.stack,
|
||||
error: err,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const notFoundHandler = (req: Request, res: Response): void => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: `Route ${req.method} ${req.path} nenájdená`,
|
||||
});
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
statusCode: number;
|
||||
isOperational: boolean;
|
||||
|
||||
constructor(message: string, statusCode = 400) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
115
backend/src/middleware/rbac.middleware.ts
Normal file
115
backend/src/middleware/rbac.middleware.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import { AuthRequest } from './auth.middleware';
|
||||
import { errorResponse } from '../utils/helpers';
|
||||
|
||||
type Resource = 'projects' | 'tasks' | 'equipment' | 'rma' | 'customers' | 'settings' | 'users' | 'logs';
|
||||
type Action = 'create' | 'read' | 'update' | 'delete' | 'all' | 'approve' | '*';
|
||||
|
||||
export const hasPermission = (
|
||||
permissions: Record<string, string[]>,
|
||||
resource: Resource,
|
||||
action: Action
|
||||
): boolean => {
|
||||
const resourcePermissions = permissions[resource];
|
||||
|
||||
if (!resourcePermissions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for wildcard permission
|
||||
if (resourcePermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for 'all' permission (grants read/create/update/delete)
|
||||
if (resourcePermissions.includes('all') && ['create', 'read', 'update', 'delete'].includes(action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific permission
|
||||
return resourcePermissions.includes(action);
|
||||
};
|
||||
|
||||
export const checkPermission = (resource: Resource, action: Action) => {
|
||||
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user) {
|
||||
errorResponse(res, 'Nie ste prihlásený.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
const { permissions } = req.user;
|
||||
|
||||
if (!hasPermission(permissions, resource, action)) {
|
||||
errorResponse(res, 'Nemáte oprávnenie na túto akciu.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// Shorthand middleware factories
|
||||
export const canCreate = (resource: Resource) => checkPermission(resource, 'create');
|
||||
export const canRead = (resource: Resource) => checkPermission(resource, 'read');
|
||||
export const canUpdate = (resource: Resource) => checkPermission(resource, 'update');
|
||||
export const canDelete = (resource: Resource) => checkPermission(resource, 'delete');
|
||||
export const canManage = (resource: Resource) => checkPermission(resource, 'all');
|
||||
|
||||
// Check if user has ROOT role
|
||||
export const isRoot = (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user) {
|
||||
errorResponse(res, 'Nie ste prihlásený.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.roleCode !== 'ROOT') {
|
||||
errorResponse(res, 'Táto akcia vyžaduje ROOT oprávnenia.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Check if user has ADMIN or ROOT role
|
||||
export const isAdmin = (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user) {
|
||||
errorResponse(res, 'Nie ste prihlásený.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['ROOT', 'ADMIN'].includes(req.user.roleCode)) {
|
||||
errorResponse(res, 'Táto akcia vyžaduje ADMIN oprávnenia.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Check ownership or admin permission
|
||||
export const isOwnerOrAdmin = (getOwnerId: (req: AuthRequest) => Promise<string | null>) => {
|
||||
return async (req: AuthRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||
if (!req.user) {
|
||||
errorResponse(res, 'Nie ste prihlásený.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Admins and ROOT can access anything
|
||||
if (['ROOT', 'ADMIN'].includes(req.user.roleCode)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ownerId = await getOwnerId(req);
|
||||
|
||||
if (ownerId === req.user.userId) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
errorResponse(res, 'Nemáte oprávnenie na túto akciu.', 403);
|
||||
} catch {
|
||||
errorResponse(res, 'Chyba pri overovaní oprávnení.', 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
71
backend/src/middleware/validate.middleware.ts
Normal file
71
backend/src/middleware/validate.middleware.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { errorResponse } from '../utils/helpers';
|
||||
|
||||
export const validate = (schema: ZodSchema) => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
schema.parse(req.body);
|
||||
next();
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> };
|
||||
const formattedErrors = zodError.issues.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
errorResponse(res, 'Validácia zlyhala.', 400, formattedErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
errorResponse(res, 'Neočakávaná chyba validácie.', 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const validateQuery = (schema: ZodSchema) => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
const result = schema.parse(req.query);
|
||||
req.query = result as typeof req.query;
|
||||
next();
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> };
|
||||
const formattedErrors = zodError.issues.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
errorResponse(res, 'Neplatné query parametre.', 400, formattedErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
errorResponse(res, 'Neočakávaná chyba validácie.', 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const validateParams = (schema: ZodSchema) => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
const result = schema.parse(req.params);
|
||||
req.params = result as typeof req.params;
|
||||
next();
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> };
|
||||
const formattedErrors = zodError.issues.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
errorResponse(res, 'Neplatné URL parametre.', 400, formattedErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
errorResponse(res, 'Neočakávaná chyba validácie.', 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
15
backend/src/routes/auth.routes.ts
Normal file
15
backend/src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import * as authController from '../controllers/auth.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { loginSchema, registerSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/register', validate(registerSchema), authController.register);
|
||||
router.post('/login', validate(loginSchema), authController.login);
|
||||
router.post('/refresh', authController.refresh);
|
||||
router.post('/logout', authenticate, authController.logout);
|
||||
router.get('/me', authenticate, authController.getMe);
|
||||
|
||||
export default router;
|
||||
25
backend/src/routes/customers.routes.ts
Normal file
25
backend/src/routes/customers.routes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import * as customersController from '../controllers/customers.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { customerSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
router.get('/', canRead('customers'), customersController.getCustomers);
|
||||
router.post('/', canCreate('customers'), validate(customerSchema), customersController.createCustomer);
|
||||
router.get('/:id', canRead('customers'), customersController.getCustomer);
|
||||
router.put('/:id', canUpdate('customers'), validate(customerSchema), customersController.updateCustomer);
|
||||
router.delete('/:id', canDelete('customers'), customersController.deleteCustomer);
|
||||
|
||||
// Customer relations
|
||||
router.get('/:id/projects', canRead('customers'), customersController.getCustomerProjects);
|
||||
router.get('/:id/equipment', canRead('customers'), customersController.getCustomerEquipment);
|
||||
router.get('/:id/rmas', canRead('customers'), customersController.getCustomerRMAs);
|
||||
|
||||
export default router;
|
||||
15
backend/src/routes/dashboard.routes.ts
Normal file
15
backend/src/routes/dashboard.routes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import * as dashboardController from '../controllers/dashboard.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/', dashboardController.getDashboard);
|
||||
router.get('/today', dashboardController.getDashboardToday);
|
||||
router.get('/week', dashboardController.getDashboardWeek);
|
||||
router.get('/stats', dashboardController.getDashboardStats);
|
||||
router.get('/reminders', dashboardController.getDashboardReminders);
|
||||
|
||||
export default router;
|
||||
25
backend/src/routes/equipment.routes.ts
Normal file
25
backend/src/routes/equipment.routes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import * as equipmentController from '../controllers/equipment.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { equipmentSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
router.get('/', canRead('equipment'), equipmentController.getEquipment);
|
||||
router.post('/', canCreate('equipment'), validate(equipmentSchema), equipmentController.createEquipment);
|
||||
router.get('/reminders', canRead('equipment'), equipmentController.getEquipmentReminders);
|
||||
router.get('/:id', canRead('equipment'), equipmentController.getEquipmentById);
|
||||
router.put('/:id', canUpdate('equipment'), equipmentController.updateEquipment);
|
||||
router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipment);
|
||||
|
||||
// Revisions
|
||||
router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions);
|
||||
router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision);
|
||||
|
||||
export default router;
|
||||
24
backend/src/routes/index.ts
Normal file
24
backend/src/routes/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import authRoutes from './auth.routes';
|
||||
import usersRoutes from './users.routes';
|
||||
import customersRoutes from './customers.routes';
|
||||
import projectsRoutes from './projects.routes';
|
||||
import tasksRoutes from './tasks.routes';
|
||||
import equipmentRoutes from './equipment.routes';
|
||||
import rmaRoutes from './rma.routes';
|
||||
import settingsRoutes from './settings.routes';
|
||||
import dashboardRoutes from './dashboard.routes';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/auth', authRoutes);
|
||||
router.use('/users', usersRoutes);
|
||||
router.use('/customers', customersRoutes);
|
||||
router.use('/projects', projectsRoutes);
|
||||
router.use('/tasks', tasksRoutes);
|
||||
router.use('/equipment', equipmentRoutes);
|
||||
router.use('/rma', rmaRoutes);
|
||||
router.use('/settings', settingsRoutes);
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
|
||||
export default router;
|
||||
30
backend/src/routes/projects.routes.ts
Normal file
30
backend/src/routes/projects.routes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import * as projectsController from '../controllers/projects.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { projectSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
router.get('/', canRead('projects'), projectsController.getProjects);
|
||||
router.post('/', canCreate('projects'), validate(projectSchema), projectsController.createProject);
|
||||
router.get('/:id', canRead('projects'), projectsController.getProject);
|
||||
router.put('/:id', canUpdate('projects'), projectsController.updateProject);
|
||||
router.delete('/:id', canDelete('projects'), projectsController.deleteProject);
|
||||
|
||||
// Status
|
||||
router.patch('/:id/status', canUpdate('projects'), projectsController.updateProjectStatus);
|
||||
|
||||
// Tasks
|
||||
router.get('/:id/tasks', canRead('projects'), projectsController.getProjectTasks);
|
||||
|
||||
// Members
|
||||
router.post('/:id/members', canUpdate('projects'), projectsController.addProjectMember);
|
||||
router.delete('/:id/members/:userId', canUpdate('projects'), projectsController.removeProjectMember);
|
||||
|
||||
export default router;
|
||||
30
backend/src/routes/rma.routes.ts
Normal file
30
backend/src/routes/rma.routes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import * as rmaController from '../controllers/rma.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete, isAdmin } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { rmaSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
router.get('/', canRead('rma'), rmaController.getRMAs);
|
||||
router.post('/', canCreate('rma'), validate(rmaSchema), rmaController.createRMA);
|
||||
router.get('/generate-number', canCreate('rma'), rmaController.generateRMANumberEndpoint);
|
||||
router.get('/:id', canRead('rma'), rmaController.getRMA);
|
||||
router.put('/:id', canUpdate('rma'), rmaController.updateRMA);
|
||||
router.delete('/:id', canDelete('rma'), rmaController.deleteRMA);
|
||||
|
||||
// Status
|
||||
router.patch('/:id/status', canUpdate('rma'), rmaController.updateRMAStatus);
|
||||
|
||||
// Approval (Admin only)
|
||||
router.patch('/:id/approve', isAdmin, rmaController.approveRMA);
|
||||
|
||||
// Comments
|
||||
router.post('/:id/comments', canRead('rma'), rmaController.addRMAComment);
|
||||
|
||||
export default router;
|
||||
78
backend/src/routes/settings.routes.ts
Normal file
78
backend/src/routes/settings.routes.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Router } from 'express';
|
||||
import * as settingsController from '../controllers/settings.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { isRoot } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Všetky endpointy vyžadujú autentifikáciu
|
||||
router.use(authenticate);
|
||||
|
||||
// === VEREJNÉ ENDPOINTY (pre všetkých prihlásených) ===
|
||||
// Task Statuses - čítanie
|
||||
router.get('/task-statuses', settingsController.getTaskStatuses);
|
||||
// Priorities - čítanie
|
||||
router.get('/priorities', settingsController.getPriorities);
|
||||
// Equipment Types - čítanie
|
||||
router.get('/equipment-types', settingsController.getEquipmentTypes);
|
||||
// Revision Types - čítanie
|
||||
router.get('/revision-types', settingsController.getRevisionTypes);
|
||||
// RMA Statuses - čítanie
|
||||
router.get('/rma-statuses', settingsController.getRMAStatuses);
|
||||
// RMA Solutions - čítanie
|
||||
router.get('/rma-solutions', settingsController.getRMASolutions);
|
||||
// Tags - čítanie
|
||||
router.get('/tags', settingsController.getTags);
|
||||
// User Roles - čítanie
|
||||
router.get('/roles', settingsController.getUserRoles);
|
||||
|
||||
// === ROOT-ONLY ENDPOINTY (vytvorenie, úprava, mazanie) ===
|
||||
router.use(isRoot);
|
||||
|
||||
// Equipment Types - správa
|
||||
router.post('/equipment-types', settingsController.createEquipmentType);
|
||||
router.put('/equipment-types/:id', settingsController.updateEquipmentType);
|
||||
router.delete('/equipment-types/:id', settingsController.deleteEquipmentType);
|
||||
|
||||
// Revision Types - správa
|
||||
router.post('/revision-types', settingsController.createRevisionType);
|
||||
router.put('/revision-types/:id', settingsController.updateRevisionType);
|
||||
router.delete('/revision-types/:id', settingsController.deleteRevisionType);
|
||||
|
||||
// RMA Statuses - správa
|
||||
router.post('/rma-statuses', settingsController.createRMAStatus);
|
||||
router.put('/rma-statuses/:id', settingsController.updateRMAStatus);
|
||||
router.delete('/rma-statuses/:id', settingsController.deleteRMAStatus);
|
||||
|
||||
// RMA Solutions - správa
|
||||
router.post('/rma-solutions', settingsController.createRMASolution);
|
||||
router.put('/rma-solutions/:id', settingsController.updateRMASolution);
|
||||
router.delete('/rma-solutions/:id', settingsController.deleteRMASolution);
|
||||
|
||||
// Task Statuses - správa
|
||||
router.post('/task-statuses', settingsController.createTaskStatus);
|
||||
router.put('/task-statuses/:id', settingsController.updateTaskStatus);
|
||||
router.delete('/task-statuses/:id', settingsController.deleteTaskStatus);
|
||||
|
||||
// Priorities - správa
|
||||
router.post('/priorities', settingsController.createPriority);
|
||||
router.put('/priorities/:id', settingsController.updatePriority);
|
||||
router.delete('/priorities/:id', settingsController.deletePriority);
|
||||
|
||||
// Tags - správa
|
||||
router.post('/tags', settingsController.createTag);
|
||||
router.put('/tags/:id', settingsController.updateTag);
|
||||
router.delete('/tags/:id', settingsController.deleteTag);
|
||||
|
||||
// User Roles - správa
|
||||
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);
|
||||
router.put('/system/:key', settingsController.updateSystemSetting);
|
||||
|
||||
export default router;
|
||||
31
backend/src/routes/tasks.routes.ts
Normal file
31
backend/src/routes/tasks.routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Router } from 'express';
|
||||
import * as tasksController from '../controllers/tasks.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { taskSchema } from '../utils/validators';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
router.get('/', canRead('tasks'), tasksController.getTasks);
|
||||
router.post('/', canCreate('tasks'), validate(taskSchema), tasksController.createTask);
|
||||
router.get('/:id', canRead('tasks'), tasksController.getTask);
|
||||
router.put('/:id', canUpdate('tasks'), tasksController.updateTask);
|
||||
router.delete('/:id', canDelete('tasks'), tasksController.deleteTask);
|
||||
|
||||
// Status
|
||||
router.patch('/:id/status', canUpdate('tasks'), tasksController.updateTaskStatus);
|
||||
|
||||
// Assignees
|
||||
router.post('/:id/assignees', canUpdate('tasks'), tasksController.addTaskAssignee);
|
||||
router.delete('/:id/assignees/:userId', canUpdate('tasks'), tasksController.removeTaskAssignee);
|
||||
|
||||
// Comments
|
||||
router.get('/:id/comments', canRead('tasks'), tasksController.getTaskComments);
|
||||
router.post('/:id/comments', canRead('tasks'), tasksController.addTaskComment);
|
||||
|
||||
export default router;
|
||||
21
backend/src/routes/users.routes.ts
Normal file
21
backend/src/routes/users.routes.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Router } from 'express';
|
||||
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';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
// Jednoduchý zoznam pre select/dropdown - prístupné všetkým autentifikovaným
|
||||
router.get('/simple', usersController.getUsersSimple);
|
||||
|
||||
router.get('/', isAdmin, usersController.getUsers);
|
||||
router.get('/:id', isAdmin, usersController.getUser);
|
||||
router.put('/:id', isAdmin, usersController.updateUser);
|
||||
router.delete('/:id', isAdmin, usersController.deleteUser);
|
||||
router.patch('/:id/role', isAdmin, usersController.updateUserRole);
|
||||
|
||||
export default router;
|
||||
195
backend/src/services/auth.service.ts
Normal file
195
backend/src/services/auth.service.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import prisma from '../config/database';
|
||||
import { generateAccessToken, generateRefreshToken, verifyRefreshToken, TokenPayload } from '../config/jwt';
|
||||
import { ApiError } from '../middleware/errorHandler';
|
||||
|
||||
interface RegisterInput {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
roleId?: string;
|
||||
}
|
||||
|
||||
interface LoginInput {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface AuthTokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
interface UserWithRole {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
role: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
permissions: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export class AuthService {
|
||||
async register(data: RegisterInput): Promise<UserWithRole> {
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: data.email },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ApiError('Používateľ s týmto emailom už existuje.', 409);
|
||||
}
|
||||
|
||||
// Get default role (USER) if not specified
|
||||
let roleId = data.roleId;
|
||||
if (!roleId) {
|
||||
const defaultRole = await prisma.userRole.findFirst({
|
||||
where: { code: 'USER', active: true },
|
||||
});
|
||||
|
||||
if (!defaultRole) {
|
||||
throw new ApiError('Predvolená rola nie je nakonfigurovaná.', 500);
|
||||
}
|
||||
|
||||
roleId = defaultRole.id;
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
// Create user
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
name: data.name,
|
||||
roleId,
|
||||
},
|
||||
include: {
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async login(data: LoginInput): Promise<{ user: UserWithRole; tokens: AuthTokens }> {
|
||||
// Find user
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: data.email },
|
||||
include: {
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError('Neplatný email alebo heslo.', 401);
|
||||
}
|
||||
|
||||
if (!user.active) {
|
||||
throw new ApiError('Váš účet je deaktivovaný.', 403);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(data.password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new ApiError('Neplatný email alebo heslo.', 401);
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokenPayload: TokenPayload = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
roleId: user.role.id,
|
||||
roleCode: user.role.code,
|
||||
};
|
||||
|
||||
const tokens: AuthTokens = {
|
||||
accessToken: generateAccessToken(tokenPayload),
|
||||
refreshToken: generateRefreshToken(tokenPayload),
|
||||
};
|
||||
|
||||
return { user, tokens };
|
||||
}
|
||||
|
||||
async refreshTokens(refreshToken: string): Promise<AuthTokens> {
|
||||
try {
|
||||
const decoded = verifyRefreshToken(refreshToken);
|
||||
|
||||
// Verify user still exists and is active
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
include: {
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.active) {
|
||||
throw new ApiError('Používateľ neexistuje alebo je neaktívny.', 403);
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
const tokenPayload: TokenPayload = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
roleId: user.role.id,
|
||||
roleCode: user.role.code,
|
||||
};
|
||||
|
||||
return {
|
||||
accessToken: generateAccessToken(tokenPayload),
|
||||
refreshToken: generateRefreshToken(tokenPayload),
|
||||
};
|
||||
} catch {
|
||||
throw new ApiError('Neplatný refresh token.', 401);
|
||||
}
|
||||
}
|
||||
|
||||
async getMe(userId: string): Promise<UserWithRole> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError('Používateľ nebol nájdený.', 404);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
211
backend/src/services/config.service.ts
Normal file
211
backend/src/services/config.service.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import prisma from '../config/database';
|
||||
|
||||
type CacheValue = {
|
||||
data: unknown;
|
||||
expiry: number;
|
||||
};
|
||||
|
||||
export class ConfigService {
|
||||
private cache = new Map<string, CacheValue>();
|
||||
private cacheExpiry = 60000; // 1 minute
|
||||
|
||||
private getCached<T>(key: string): T | null {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached && cached.expiry > Date.now()) {
|
||||
return cached.data as T;
|
||||
}
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
private setCache(key: string, data: unknown): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expiry: Date.now() + this.cacheExpiry,
|
||||
});
|
||||
}
|
||||
|
||||
async getEquipmentTypes(activeOnly = true) {
|
||||
const cacheKey = `equipment_types_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const types = await prisma.equipmentType.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, types);
|
||||
return types;
|
||||
}
|
||||
|
||||
async getRevisionTypes(activeOnly = true) {
|
||||
const cacheKey = `revision_types_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const types = await prisma.revisionType.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, types);
|
||||
return types;
|
||||
}
|
||||
|
||||
async getRMAStatuses(activeOnly = true) {
|
||||
const cacheKey = `rma_statuses_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const statuses = await prisma.rMAStatus.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, statuses);
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async getRMASolutions(activeOnly = true) {
|
||||
const cacheKey = `rma_solutions_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const solutions = await prisma.rMASolution.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, solutions);
|
||||
return solutions;
|
||||
}
|
||||
|
||||
async getTaskStatuses(activeOnly = true) {
|
||||
const cacheKey = `task_statuses_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const statuses = await prisma.taskStatus.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, statuses);
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async getPriorities(activeOnly = true) {
|
||||
const cacheKey = `priorities_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const priorities = await prisma.priority.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, priorities);
|
||||
return priorities;
|
||||
}
|
||||
|
||||
async getUserRoles(activeOnly = true) {
|
||||
const cacheKey = `user_roles_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const roles = await prisma.userRole.findMany({
|
||||
where: activeOnly ? { active: true } : {},
|
||||
orderBy: { level: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, roles);
|
||||
return roles;
|
||||
}
|
||||
|
||||
async getTags(entityType?: string, activeOnly = true) {
|
||||
const cacheKey = `tags_${entityType || 'all'}_${activeOnly}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const tags = await prisma.tag.findMany({
|
||||
where: {
|
||||
...(activeOnly && { active: true }),
|
||||
...(entityType && { entityType }),
|
||||
},
|
||||
orderBy: { order: 'asc' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, tags);
|
||||
return tags;
|
||||
}
|
||||
|
||||
async getSetting<T = unknown>(key: string): Promise<T | null> {
|
||||
const cacheKey = `setting_${key}`;
|
||||
|
||||
const cached = this.getCached<T>(cacheKey);
|
||||
if (cached !== null) return cached;
|
||||
|
||||
const setting = await prisma.systemSetting.findUnique({
|
||||
where: { key },
|
||||
});
|
||||
|
||||
if (setting) {
|
||||
this.setCache(cacheKey, setting.value);
|
||||
return setting.value as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getSettingsByCategory(category: string) {
|
||||
const cacheKey = `settings_category_${category}`;
|
||||
|
||||
const cached = this.getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const settings = await prisma.systemSetting.findMany({
|
||||
where: { category },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
async getInitialTaskStatus() {
|
||||
const statuses = await this.getTaskStatuses();
|
||||
return (statuses as { id: string; isInitial: boolean }[]).find((s) => s.isInitial);
|
||||
}
|
||||
|
||||
async getInitialRMAStatus() {
|
||||
const statuses = await this.getRMAStatuses();
|
||||
return (statuses as { id: string; isInitial: boolean }[]).find((s) => s.isInitial);
|
||||
}
|
||||
|
||||
async getDefaultPriority() {
|
||||
const priorities = await this.getPriorities();
|
||||
return (priorities as { id: string; code: string }[]).find((p) => p.code === 'MEDIUM');
|
||||
}
|
||||
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
clearCacheKey(keyPrefix: string): void {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.startsWith(keyPrefix)) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService();
|
||||
73
backend/src/utils/helpers.ts
Normal file
73
backend/src/utils/helpers.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export const getParam = (req: Request, name: string): string => {
|
||||
const value = req.params[name];
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
};
|
||||
|
||||
export const getQueryString = (req: Request, name: string): string | undefined => {
|
||||
const value = req.query[name];
|
||||
if (value === undefined) return undefined;
|
||||
return Array.isArray(value) ? String(value[0]) : String(value);
|
||||
};
|
||||
|
||||
export const successResponse = <T>(
|
||||
res: Response,
|
||||
data: T,
|
||||
message = 'Success',
|
||||
statusCode = 200
|
||||
) => {
|
||||
return res.status(statusCode).json({
|
||||
success: true,
|
||||
message,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const errorResponse = (
|
||||
res: Response,
|
||||
message: string,
|
||||
statusCode = 400,
|
||||
errors?: unknown
|
||||
) => {
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
errors,
|
||||
});
|
||||
};
|
||||
|
||||
export const paginatedResponse = <T>(
|
||||
res: Response,
|
||||
data: T[],
|
||||
total: number,
|
||||
page: number,
|
||||
limit: number
|
||||
) => {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasNext: page * limit < total,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const parseQueryInt = (value: unknown, defaultValue: number): number => {
|
||||
if (typeof value === 'string') {
|
||||
const parsed = parseInt(value, 10);
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const parseBooleanQuery = (value: unknown): boolean | undefined => {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
return undefined;
|
||||
};
|
||||
183
backend/src/utils/validators.ts
Normal file
183
backend/src/utils/validators.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Auth validators
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email('Neplatný email'),
|
||||
password: z.string().min(1, 'Heslo je povinné'),
|
||||
});
|
||||
|
||||
export const registerSchema = 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().optional(),
|
||||
});
|
||||
|
||||
// User validators
|
||||
export const updateUserSchema = z.object({
|
||||
email: z.string().email('Neplatný email').optional(),
|
||||
name: z.string().min(2, 'Meno musí mať aspoň 2 znaky').optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Customer validators
|
||||
export const customerSchema = z.object({
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
address: z.string().optional(),
|
||||
email: z.string().email('Neplatný email').optional().or(z.literal('')),
|
||||
phone: z.string().optional(),
|
||||
ico: z.string().optional(),
|
||||
dic: z.string().optional(),
|
||||
icdph: z.string().optional(),
|
||||
contactPerson: z.string().optional(),
|
||||
contactEmail: z.string().email('Neplatný email').optional().or(z.literal('')),
|
||||
contactPhone: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
// Project validators
|
||||
export const projectSchema = z.object({
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
customerId: z.string().optional(),
|
||||
statusId: z.string().optional(),
|
||||
softDeadline: z.string().datetime().optional().or(z.literal('')),
|
||||
hardDeadline: z.string().datetime().optional().or(z.literal('')),
|
||||
});
|
||||
|
||||
// Task validators
|
||||
export const taskSchema = z.object({
|
||||
title: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
parentId: z.string().optional(),
|
||||
statusId: z.string().optional(),
|
||||
priorityId: z.string().optional(),
|
||||
deadline: z.string().datetime().optional().or(z.literal('')),
|
||||
assigneeIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
// Equipment validators
|
||||
export const equipmentSchema = z.object({
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
typeId: z.string().min(1, 'Typ je povinný'),
|
||||
brand: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
customerId: z.string().optional(),
|
||||
address: z.string().min(1, 'Adresa je povinná'),
|
||||
location: z.string().optional(),
|
||||
partNumber: z.string().optional(),
|
||||
serialNumber: z.string().optional(),
|
||||
installDate: z.string().datetime().optional().or(z.literal('')),
|
||||
warrantyEnd: z.string().datetime().optional().or(z.literal('')),
|
||||
warrantyStatus: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
// RMA validators
|
||||
export const rmaSchema = z.object({
|
||||
customerId: z.string().optional(),
|
||||
customerName: z.string().optional(),
|
||||
customerAddress: z.string().optional(),
|
||||
customerEmail: z.string().email().optional().or(z.literal('')),
|
||||
customerPhone: z.string().optional(),
|
||||
customerICO: z.string().optional(),
|
||||
submittedBy: z.string().min(1, 'Meno odosielateľa je povinné'),
|
||||
productName: z.string().min(1, 'Názov produktu je povinný'),
|
||||
invoiceNumber: z.string().optional(),
|
||||
purchaseDate: z.string().datetime().optional().or(z.literal('')),
|
||||
productNumber: z.string().optional(),
|
||||
serialNumber: z.string().optional(),
|
||||
accessories: z.string().optional(),
|
||||
issueDescription: z.string().min(1, 'Popis problému je povinný'),
|
||||
statusId: z.string().optional(),
|
||||
proposedSolutionId: z.string().optional(),
|
||||
});
|
||||
|
||||
// Settings validators
|
||||
export const equipmentTypeSchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
order: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const revisionTypeSchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
intervalDays: z.number().min(0, 'Interval musí byť kladné číslo'),
|
||||
reminderDays: z.number().min(0).optional(),
|
||||
color: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
order: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const taskStatusSchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
order: z.number().optional(),
|
||||
swimlaneColumn: z.string().optional(),
|
||||
isInitial: z.boolean().optional(),
|
||||
isFinal: z.boolean().optional(),
|
||||
canTransitionTo: z.array(z.string()).optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const prioritySchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
level: z.number().min(1).max(10),
|
||||
order: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const userRoleSchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
permissions: z.record(z.string(), z.array(z.string())),
|
||||
level: z.number().min(1),
|
||||
order: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const systemSettingSchema = z.object({
|
||||
key: z.string().min(1, 'Kľúč je povinný'),
|
||||
value: z.unknown(),
|
||||
category: z.string().min(1, 'Kategória je povinná'),
|
||||
label: z.string().min(1, 'Label je povinný'),
|
||||
description: z.string().optional(),
|
||||
dataType: z.enum(['string', 'number', 'boolean', 'json']),
|
||||
validation: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const tagSchema = z.object({
|
||||
code: z.string().min(1, 'Kód je povinný'),
|
||||
name: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
entityType: z.enum(['PROJECT', 'TASK', 'EQUIPMENT', 'RMA']),
|
||||
order: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Pagination
|
||||
export const paginationSchema = z.object({
|
||||
page: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 1)),
|
||||
limit: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 20)),
|
||||
});
|
||||
Reference in New Issue
Block a user