feat: AI Kurzy module, project/service documents, services SQL import

- Add AI Kurzy module with courses, participants, and registrations management
- Add project documents and service documents features
- Add service folders for document organization
- Add SQL import queries for services from firmy.slovensko.ai
- Update todo notifications and group messaging
- Various API improvements and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-21 11:32:49 +01:00
parent d9f16ad0a6
commit 4089bb4be2
37 changed files with 7514 additions and 35 deletions

View File

@@ -0,0 +1,243 @@
import * as aiKurzyService from '../services/ai-kurzy.service.js';
// ==================== KURZY ====================
export const getAllKurzy = async (req, res, next) => {
try {
const kurzy = await aiKurzyService.getAllKurzy();
res.json({ data: kurzy });
} catch (error) {
next(error);
}
};
export const getKurzById = async (req, res, next) => {
try {
const { kurzId } = req.params;
const kurz = await aiKurzyService.getKurzById(parseInt(kurzId));
res.json({ data: kurz });
} catch (error) {
next(error);
}
};
export const createKurz = async (req, res, next) => {
try {
const kurz = await aiKurzyService.createKurz(req.body);
res.status(201).json({ data: kurz });
} catch (error) {
next(error);
}
};
export const updateKurz = async (req, res, next) => {
try {
const { kurzId } = req.params;
const kurz = await aiKurzyService.updateKurz(parseInt(kurzId), req.body);
res.json({ data: kurz });
} catch (error) {
next(error);
}
};
export const deleteKurz = async (req, res, next) => {
try {
const { kurzId } = req.params;
const result = await aiKurzyService.deleteKurz(parseInt(kurzId));
res.json({ data: result });
} catch (error) {
next(error);
}
};
// ==================== UCASTNICI ====================
export const getAllUcastnici = async (req, res, next) => {
try {
const ucastnici = await aiKurzyService.getAllUcastnici();
res.json({ data: ucastnici });
} catch (error) {
next(error);
}
};
export const getUcastnikById = async (req, res, next) => {
try {
const { ucastnikId } = req.params;
const ucastnik = await aiKurzyService.getUcastnikById(parseInt(ucastnikId));
res.json({ data: ucastnik });
} catch (error) {
next(error);
}
};
export const createUcastnik = async (req, res, next) => {
try {
const ucastnik = await aiKurzyService.createUcastnik(req.body);
res.status(201).json({ data: ucastnik });
} catch (error) {
next(error);
}
};
export const updateUcastnik = async (req, res, next) => {
try {
const { ucastnikId } = req.params;
const ucastnik = await aiKurzyService.updateUcastnik(parseInt(ucastnikId), req.body);
res.json({ data: ucastnik });
} catch (error) {
next(error);
}
};
export const deleteUcastnik = async (req, res, next) => {
try {
const { ucastnikId } = req.params;
const result = await aiKurzyService.deleteUcastnik(parseInt(ucastnikId));
res.json({ data: result });
} catch (error) {
next(error);
}
};
// ==================== REGISTRACIE ====================
export const getAllRegistracie = async (req, res, next) => {
try {
const { kurzId } = req.query;
const registracie = await aiKurzyService.getAllRegistracie(kurzId ? parseInt(kurzId) : null);
res.json({ data: registracie });
} catch (error) {
next(error);
}
};
export const getRegistraciaById = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const registracia = await aiKurzyService.getRegistraciaById(parseInt(registraciaId));
res.json({ data: registracia });
} catch (error) {
next(error);
}
};
export const createRegistracia = async (req, res, next) => {
try {
const registracia = await aiKurzyService.createRegistracia(req.body);
res.status(201).json({ data: registracia });
} catch (error) {
next(error);
}
};
export const updateRegistracia = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const registracia = await aiKurzyService.updateRegistracia(parseInt(registraciaId), req.body);
res.json({ data: registracia });
} catch (error) {
next(error);
}
};
export const deleteRegistracia = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const result = await aiKurzyService.deleteRegistracia(parseInt(registraciaId));
res.json({ data: result });
} catch (error) {
next(error);
}
};
// ==================== COMBINED TABLE (Excel-style) ====================
export const getCombinedTable = async (req, res, next) => {
try {
const data = await aiKurzyService.getCombinedTableData();
res.json({ data });
} catch (error) {
next(error);
}
};
export const updateField = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const { field, value } = req.body;
const result = await aiKurzyService.updateField(parseInt(registraciaId), field, value);
res.json({ data: result });
} catch (error) {
next(error);
}
};
// ==================== PRILOHY (Documents) ====================
export const getPrilohy = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const prilohy = await aiKurzyService.getPrilohyByRegistracia(parseInt(registraciaId));
res.json({ data: prilohy });
} catch (error) {
next(error);
}
};
export const uploadPriloha = async (req, res, next) => {
try {
const { registraciaId } = req.params;
const file = req.file;
if (!file) {
return res.status(400).json({ message: 'Súbor je povinný' });
}
const priloha = await aiKurzyService.createPriloha({
registraciaId: parseInt(registraciaId),
nazovSuboru: file.originalname,
typPrilohy: req.body.typPrilohy || 'ine',
cestaKSuboru: file.path,
mimeType: file.mimetype,
velkostSuboru: file.size,
popis: req.body.popis || null,
});
res.status(201).json({ data: priloha });
} catch (error) {
next(error);
}
};
export const deletePriloha = async (req, res, next) => {
try {
const { prilohaId } = req.params;
const result = await aiKurzyService.deletePriloha(parseInt(prilohaId));
// Optionally delete the file from disk
if (result.filePath) {
const fs = await import('fs/promises');
try {
await fs.unlink(result.filePath);
} catch {
// File might not exist, ignore
}
}
res.json({ data: { success: true } });
} catch (error) {
next(error);
}
};
// ==================== STATISTICS ====================
export const getStats = async (req, res, next) => {
try {
const stats = await aiKurzyService.getKurzyStats();
res.json({ data: stats });
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,88 @@
import * as projectDocumentService from '../services/project-document.service.js';
/**
* Get all documents for a project
* GET /api/projects/:projectId/documents
*/
export const getDocuments = async (req, res, next) => {
try {
const { projectId } = req.params;
const documents = await projectDocumentService.getDocumentsByProjectId(projectId);
res.status(200).json({
success: true,
count: documents.length,
data: documents,
});
} catch (error) {
next(error);
}
};
/**
* Upload a document for a project
* POST /api/projects/:projectId/documents
*/
export const uploadDocument = async (req, res, next) => {
try {
const { projectId } = req.params;
const userId = req.userId;
const { description } = req.body;
const document = await projectDocumentService.uploadDocument({
projectId,
userId,
file: req.file,
description,
});
res.status(201).json({
success: true,
data: document,
message: 'Dokument bol nahraný',
});
} catch (error) {
next(error);
}
};
/**
* Download a document
* GET /api/projects/:projectId/documents/:docId/download
*/
export const downloadDocument = async (req, res, next) => {
try {
const { projectId, docId } = req.params;
const { filePath, fileName, fileType } = await projectDocumentService.getDocumentForDownload(
projectId,
docId
);
res.setHeader('Content-Type', fileType);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
res.sendFile(filePath);
} catch (error) {
next(error);
}
};
/**
* Delete a document
* DELETE /api/projects/:projectId/documents/:docId
*/
export const deleteDocument = async (req, res, next) => {
try {
const { projectId, docId } = req.params;
const result = await projectDocumentService.deleteDocument(projectId, docId);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,54 @@
import * as serviceDocumentService from '../services/service-document.service.js';
export const getDocumentsByFolderId = async (req, res, next) => {
try {
const { folderId } = req.params;
const documents = await serviceDocumentService.getDocumentsByFolderId(folderId);
res.json({ data: documents });
} catch (error) {
next(error);
}
};
export const uploadDocument = async (req, res, next) => {
try {
const { folderId } = req.params;
const userId = req.user.id;
const file = req.file;
const { description } = req.body;
const document = await serviceDocumentService.uploadDocument({
folderId,
userId,
file,
description,
});
res.status(201).json({ data: document });
} catch (error) {
next(error);
}
};
export const downloadDocument = async (req, res, next) => {
try {
const { folderId, documentId } = req.params;
const { filePath, fileName, fileType } = await serviceDocumentService.getDocumentForDownload(folderId, documentId);
res.setHeader('Content-Type', fileType);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
res.sendFile(filePath);
} catch (error) {
next(error);
}
};
export const deleteDocument = async (req, res, next) => {
try {
const { folderId, documentId } = req.params;
const result = await serviceDocumentService.deleteDocument(folderId, documentId);
res.json({ data: result });
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,52 @@
import * as serviceFolderService from '../services/service-folder.service.js';
export const getAllFolders = async (req, res, next) => {
try {
const folders = await serviceFolderService.getAllFolders();
res.json({ data: folders });
} catch (error) {
next(error);
}
};
export const getFolderById = async (req, res, next) => {
try {
const { folderId } = req.params;
const folder = await serviceFolderService.getFolderById(folderId);
res.json({ data: folder });
} catch (error) {
next(error);
}
};
export const createFolder = async (req, res, next) => {
try {
const { name } = req.body;
const userId = req.user.id;
const folder = await serviceFolderService.createFolder({ name, userId });
res.status(201).json({ data: folder });
} catch (error) {
next(error);
}
};
export const updateFolder = async (req, res, next) => {
try {
const { folderId } = req.params;
const { name } = req.body;
const folder = await serviceFolderService.updateFolder(folderId, { name });
res.json({ data: folder });
} catch (error) {
next(error);
}
};
export const deleteFolder = async (req, res, next) => {
try {
const { folderId } = req.params;
const result = await serviceFolderService.deleteFolder(folderId);
res.json({ data: result });
} catch (error) {
next(error);
}
};

View File

@@ -231,3 +231,81 @@ export const toggleTodo = async (req, res, next) => {
next(error);
}
};
/**
* Get overdue todos count
* GET /api/todos/overdue-count
*/
export const getOverdueCount = async (req, res, next) => {
try {
const userId = req.userId;
const userRole = req.user?.role;
const count = await todoService.getOverdueCount(userId, userRole);
res.status(200).json({
success: true,
data: { overdueCount: count },
});
} catch (error) {
next(error);
}
};
/**
* Get completed todos created by current user count
* GET /api/todos/completed-by-me
*/
export const getCompletedByMeCount = async (req, res, next) => {
try {
const userId = req.userId;
const count = await todoService.getCompletedByMeCount(userId);
res.status(200).json({
success: true,
data: { completedByMeCount: count },
});
} catch (error) {
next(error);
}
};
/**
* Get combined todo counts for sidebar badges
* GET /api/todos/counts
*/
export const getTodoCounts = async (req, res, next) => {
try {
const userId = req.userId;
const userRole = req.user?.role;
const counts = await todoService.getTodoCounts(userId, userRole);
res.status(200).json({
success: true,
data: counts,
});
} catch (error) {
next(error);
}
};
/**
* Mark all completed todos created by the user as notified
* POST /api/todos/mark-completed-notified
*/
export const markCompletedAsNotified = async (req, res, next) => {
try {
const userId = req.userId;
await todoService.markCompletedAsNotified(userId);
res.status(200).json({
success: true,
message: 'Completed todos marked as notified',
});
} catch (error) {
next(error);
}
};