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:
232
src/routes/ai-kurzy.routes.js
Normal file
232
src/routes/ai-kurzy.routes.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as aiKurzyController from '../controllers/ai-kurzy.controller.js';
|
||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||
import { validateBody, validateParams, validateQuery } from '../middlewares/security/validateInput.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configure multer for file uploads
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'ai-kurzy');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
cb(null, uniqueSuffix + '-' + file.originalname);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
||||
});
|
||||
|
||||
// Validation schemas
|
||||
const kurzIdSchema = z.object({
|
||||
kurzId: z.string().regex(/^\d+$/),
|
||||
});
|
||||
|
||||
const ucastnikIdSchema = z.object({
|
||||
ucastnikId: z.string().regex(/^\d+$/),
|
||||
});
|
||||
|
||||
const registraciaIdSchema = z.object({
|
||||
registraciaId: z.string().regex(/^\d+$/),
|
||||
});
|
||||
|
||||
const createKurzSchema = z.object({
|
||||
nazov: z.string().min(1).max(255),
|
||||
typKurzu: z.string().min(1).max(100),
|
||||
popis: z.string().optional().nullable(),
|
||||
cena: z.string().or(z.number()),
|
||||
maxKapacita: z.number().int().positive().optional().nullable(),
|
||||
aktivny: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateKurzSchema = createKurzSchema.partial();
|
||||
|
||||
const createUcastnikSchema = z.object({
|
||||
titul: z.string().max(50).optional().nullable(),
|
||||
meno: z.string().min(1).max(100),
|
||||
priezvisko: z.string().min(1).max(100),
|
||||
email: z.string().email().max(255),
|
||||
telefon: z.string().max(50).optional().nullable(),
|
||||
firma: z.string().max(255).optional().nullable(),
|
||||
mesto: z.string().max(100).optional().nullable(),
|
||||
ulica: z.string().max(255).optional().nullable(),
|
||||
psc: z.string().max(10).optional().nullable(),
|
||||
});
|
||||
|
||||
const updateUcastnikSchema = createUcastnikSchema.partial();
|
||||
|
||||
const createRegistraciaSchema = z.object({
|
||||
kurzId: z.number().int().positive(),
|
||||
ucastnikId: z.number().int().positive(),
|
||||
datumOd: z.string().optional().nullable(),
|
||||
datumDo: z.string().optional().nullable(),
|
||||
formaKurzu: z.enum(['prezencne', 'online', 'hybridne']).optional(),
|
||||
pocetUcastnikov: z.number().int().positive().optional(),
|
||||
fakturaCislo: z.string().max(100).optional().nullable(),
|
||||
fakturaVystavena: z.boolean().optional(),
|
||||
zaplatene: z.boolean().optional(),
|
||||
stav: z.enum(['potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny']).optional(),
|
||||
poznamka: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
const updateRegistraciaSchema = createRegistraciaSchema.partial();
|
||||
|
||||
const registracieQuerySchema = z.object({
|
||||
kurzId: z.string().regex(/^\d+$/).optional(),
|
||||
});
|
||||
|
||||
// All routes require authentication and admin role
|
||||
router.use(authenticate);
|
||||
router.use(requireAdmin);
|
||||
|
||||
// ==================== STATISTICS ====================
|
||||
|
||||
router.get('/stats', aiKurzyController.getStats);
|
||||
|
||||
// ==================== KURZY ====================
|
||||
|
||||
router.get('/kurzy', aiKurzyController.getAllKurzy);
|
||||
|
||||
router.post(
|
||||
'/kurzy',
|
||||
validateBody(createKurzSchema),
|
||||
aiKurzyController.createKurz
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/kurzy/:kurzId',
|
||||
validateParams(kurzIdSchema),
|
||||
aiKurzyController.getKurzById
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/kurzy/:kurzId',
|
||||
validateParams(kurzIdSchema),
|
||||
validateBody(updateKurzSchema),
|
||||
aiKurzyController.updateKurz
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/kurzy/:kurzId',
|
||||
validateParams(kurzIdSchema),
|
||||
aiKurzyController.deleteKurz
|
||||
);
|
||||
|
||||
// ==================== UCASTNICI ====================
|
||||
|
||||
router.get('/ucastnici', aiKurzyController.getAllUcastnici);
|
||||
|
||||
router.post(
|
||||
'/ucastnici',
|
||||
validateBody(createUcastnikSchema),
|
||||
aiKurzyController.createUcastnik
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/ucastnici/:ucastnikId',
|
||||
validateParams(ucastnikIdSchema),
|
||||
aiKurzyController.getUcastnikById
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/ucastnici/:ucastnikId',
|
||||
validateParams(ucastnikIdSchema),
|
||||
validateBody(updateUcastnikSchema),
|
||||
aiKurzyController.updateUcastnik
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/ucastnici/:ucastnikId',
|
||||
validateParams(ucastnikIdSchema),
|
||||
aiKurzyController.deleteUcastnik
|
||||
);
|
||||
|
||||
// ==================== REGISTRACIE ====================
|
||||
|
||||
router.get(
|
||||
'/registracie',
|
||||
validateQuery(registracieQuerySchema),
|
||||
aiKurzyController.getAllRegistracie
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/registracie',
|
||||
validateBody(createRegistraciaSchema),
|
||||
aiKurzyController.createRegistracia
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/registracie/:registraciaId',
|
||||
validateParams(registraciaIdSchema),
|
||||
aiKurzyController.getRegistraciaById
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/registracie/:registraciaId',
|
||||
validateParams(registraciaIdSchema),
|
||||
validateBody(updateRegistraciaSchema),
|
||||
aiKurzyController.updateRegistracia
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/registracie/:registraciaId',
|
||||
validateParams(registraciaIdSchema),
|
||||
aiKurzyController.deleteRegistracia
|
||||
);
|
||||
|
||||
// ==================== COMBINED TABLE (Excel-style) ====================
|
||||
|
||||
router.get('/table', aiKurzyController.getCombinedTable);
|
||||
|
||||
const updateFieldSchema = z.object({
|
||||
field: z.string(),
|
||||
value: z.any(),
|
||||
});
|
||||
|
||||
router.patch(
|
||||
'/table/:registraciaId/field',
|
||||
validateParams(registraciaIdSchema),
|
||||
validateBody(updateFieldSchema),
|
||||
aiKurzyController.updateField
|
||||
);
|
||||
|
||||
// ==================== PRILOHY (Documents) ====================
|
||||
|
||||
const prilohaIdSchema = z.object({
|
||||
prilohaId: z.string().regex(/^\d+$/),
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/registracie/:registraciaId/prilohy',
|
||||
validateParams(registraciaIdSchema),
|
||||
aiKurzyController.getPrilohy
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/registracie/:registraciaId/prilohy',
|
||||
validateParams(registraciaIdSchema),
|
||||
upload.single('file'),
|
||||
aiKurzyController.uploadPriloha
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/prilohy/:prilohaId',
|
||||
validateParams(prilohaIdSchema),
|
||||
aiKurzyController.deletePriloha
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,5 +1,7 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import * as projectController from '../controllers/project.controller.js';
|
||||
import * as projectDocumentController from '../controllers/project-document.controller.js';
|
||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||
import { checkProjectAccess } from '../middlewares/auth/resourceAccessMiddleware.js';
|
||||
@@ -7,6 +9,14 @@ import { validateBody, validateParams } from '../middlewares/security/validateIn
|
||||
import { createProjectSchema, updateProjectSchema } from '../validators/crm.validators.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Configure multer for file uploads (memory storage)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB max
|
||||
},
|
||||
});
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All project routes require authentication
|
||||
@@ -136,4 +146,40 @@ router.delete(
|
||||
projectController.removeUserFromProject
|
||||
);
|
||||
|
||||
// Project Documents
|
||||
router.get(
|
||||
'/:projectId/documents',
|
||||
validateParams(z.object({ projectId: z.string().uuid() })),
|
||||
checkProjectAccess,
|
||||
projectDocumentController.getDocuments
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:projectId/documents',
|
||||
validateParams(z.object({ projectId: z.string().uuid() })),
|
||||
checkProjectAccess,
|
||||
upload.single('file'),
|
||||
projectDocumentController.uploadDocument
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:projectId/documents/:docId/download',
|
||||
validateParams(z.object({
|
||||
projectId: z.string().uuid(),
|
||||
docId: z.string().uuid()
|
||||
})),
|
||||
checkProjectAccess,
|
||||
projectDocumentController.downloadDocument
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:projectId/documents/:docId',
|
||||
requireAdmin,
|
||||
validateParams(z.object({
|
||||
projectId: z.string().uuid(),
|
||||
docId: z.string().uuid()
|
||||
})),
|
||||
projectDocumentController.deleteDocument
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import * as serviceController from '../controllers/service.controller.js';
|
||||
import * as serviceFolderController from '../controllers/service-folder.controller.js';
|
||||
import * as serviceDocumentController from '../controllers/service-document.controller.js';
|
||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
||||
@@ -8,27 +11,129 @@ import { z } from 'zod';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
|
||||
});
|
||||
|
||||
const serviceIdSchema = z.object({
|
||||
serviceId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const folderIdSchema = z.object({
|
||||
folderId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const folderDocumentIdSchema = z.object({
|
||||
folderId: z.string().uuid(),
|
||||
documentId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const createFolderSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
});
|
||||
|
||||
const updateFolderSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
});
|
||||
|
||||
// All service routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// ==================== SERVICE FOLDERS (must be before :serviceId routes) ====================
|
||||
|
||||
/**
|
||||
* GET /api/services/folders - Get all folders (all authenticated users)
|
||||
*/
|
||||
router.get('/folders', serviceFolderController.getAllFolders);
|
||||
|
||||
/**
|
||||
* POST /api/services/folders - Create new folder (admin only)
|
||||
*/
|
||||
router.post(
|
||||
'/folders',
|
||||
requireAdmin,
|
||||
validateBody(createFolderSchema),
|
||||
serviceFolderController.createFolder
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/services/folders/:folderId - Get folder by ID (all authenticated users)
|
||||
*/
|
||||
router.get(
|
||||
'/folders/:folderId',
|
||||
validateParams(folderIdSchema),
|
||||
serviceFolderController.getFolderById
|
||||
);
|
||||
|
||||
/**
|
||||
* PUT /api/services/folders/:folderId - Update folder (admin only)
|
||||
*/
|
||||
router.put(
|
||||
'/folders/:folderId',
|
||||
requireAdmin,
|
||||
validateParams(folderIdSchema),
|
||||
validateBody(updateFolderSchema),
|
||||
serviceFolderController.updateFolder
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/services/folders/:folderId - Delete folder (admin only)
|
||||
*/
|
||||
router.delete(
|
||||
'/folders/:folderId',
|
||||
requireAdmin,
|
||||
validateParams(folderIdSchema),
|
||||
serviceFolderController.deleteFolder
|
||||
);
|
||||
|
||||
// ==================== SERVICE DOCUMENTS ====================
|
||||
|
||||
/**
|
||||
* GET /api/services/folders/:folderId/documents - Get all documents in folder
|
||||
*/
|
||||
router.get(
|
||||
'/folders/:folderId/documents',
|
||||
validateParams(folderIdSchema),
|
||||
serviceDocumentController.getDocumentsByFolderId
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/services/folders/:folderId/documents - Upload document to folder
|
||||
*/
|
||||
router.post(
|
||||
'/folders/:folderId/documents',
|
||||
validateParams(folderIdSchema),
|
||||
upload.single('file'),
|
||||
serviceDocumentController.uploadDocument
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/services/folders/:folderId/documents/:documentId/download - Download document
|
||||
*/
|
||||
router.get(
|
||||
'/folders/:folderId/documents/:documentId/download',
|
||||
validateParams(folderDocumentIdSchema),
|
||||
serviceDocumentController.downloadDocument
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/services/folders/:folderId/documents/:documentId - Delete document (admin only)
|
||||
*/
|
||||
router.delete(
|
||||
'/folders/:folderId/documents/:documentId',
|
||||
requireAdmin,
|
||||
validateParams(folderDocumentIdSchema),
|
||||
serviceDocumentController.deleteDocument
|
||||
);
|
||||
|
||||
// ==================== SERVICES ====================
|
||||
|
||||
/**
|
||||
* GET /api/services - Get all services (all authenticated users)
|
||||
*/
|
||||
router.get('/', serviceController.getAllServices);
|
||||
|
||||
/**
|
||||
* GET /api/services/:serviceId - Get service by ID (all authenticated users)
|
||||
*/
|
||||
router.get(
|
||||
'/:serviceId',
|
||||
validateParams(serviceIdSchema),
|
||||
serviceController.getServiceById
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/services - Create new service (admin only)
|
||||
*/
|
||||
@@ -39,6 +144,15 @@ router.post(
|
||||
serviceController.createService
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/services/:serviceId - Get service by ID (all authenticated users)
|
||||
*/
|
||||
router.get(
|
||||
'/:serviceId',
|
||||
validateParams(serviceIdSchema),
|
||||
serviceController.getServiceById
|
||||
);
|
||||
|
||||
/**
|
||||
* PUT /api/services/:serviceId - Update service (admin only)
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,18 @@ router.use(authenticate);
|
||||
// Get all todos
|
||||
router.get('/', todoController.getAllTodos);
|
||||
|
||||
// Get combined todo counts (overdue + completed by me) for sidebar badges
|
||||
router.get('/counts', todoController.getTodoCounts);
|
||||
|
||||
// Get overdue todos count
|
||||
router.get('/overdue-count', todoController.getOverdueCount);
|
||||
|
||||
// Get completed todos created by current user
|
||||
router.get('/completed-by-me', todoController.getCompletedByMeCount);
|
||||
|
||||
// Mark completed todos as notified (when user opens Todos page)
|
||||
router.post('/mark-completed-notified', todoController.markCompletedAsNotified);
|
||||
|
||||
// Get todo by ID
|
||||
router.get(
|
||||
'/:todoId',
|
||||
|
||||
Reference in New Issue
Block a user