feat: Multi-feature CRM update
- Add team_leader role with appropriate permissions - Add lastSeen timestamp for chat online indicator - Add needsFollowup flag to ucastnici table - Add getTodayCalendarCount endpoint for calendar badge - Add company reminders to calendar data - Enhance company search to include phone and contacts - Update routes to allow team_leader access to kurzy, services, timesheets Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,3 +128,23 @@ export const sendEventNotification = async (req, res, next) => {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of events and todos for today (for calendar badge)
|
||||||
|
* GET /api/events/today-count
|
||||||
|
*/
|
||||||
|
export const getTodayCount = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'team_leader';
|
||||||
|
|
||||||
|
const counts = await eventService.getTodayCalendarCount(userId, isAdmin);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: counts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer, serial, varchar, numeric, date, bigint, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer, serial, varchar, numeric, date, bigint, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
// Enums
|
// Enums
|
||||||
export const roleEnum = pgEnum('role', ['admin', 'member']);
|
export const roleEnum = pgEnum('role', ['admin', 'team_leader', 'member']);
|
||||||
export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']);
|
export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']);
|
||||||
export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']);
|
export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']);
|
||||||
export const todoPriorityEnum = pgEnum('todo_priority', ['low', 'medium', 'high', 'urgent']);
|
export const todoPriorityEnum = pgEnum('todo_priority', ['low', 'medium', 'high', 'urgent']);
|
||||||
@@ -18,6 +18,7 @@ export const users = pgTable('users', {
|
|||||||
changedPassword: boolean('changed_password').default(false),
|
changedPassword: boolean('changed_password').default(false),
|
||||||
role: roleEnum('role').default('member').notNull(),
|
role: roleEnum('role').default('member').notNull(),
|
||||||
lastLogin: timestamp('last_login'),
|
lastLogin: timestamp('last_login'),
|
||||||
|
lastSeen: timestamp('last_seen'),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
@@ -460,6 +461,7 @@ export const ucastnici = pgTable('ucastnici', {
|
|||||||
mesto: varchar('mesto', { length: 100 }),
|
mesto: varchar('mesto', { length: 100 }),
|
||||||
ulica: varchar('ulica', { length: 255 }),
|
ulica: varchar('ulica', { length: 255 }),
|
||||||
psc: varchar('psc', { length: 10 }),
|
psc: varchar('psc', { length: 10 }),
|
||||||
|
needsFollowup: boolean('needs_followup').default(false).notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
|
|||||||
@@ -38,3 +38,7 @@ export const requireRole = (...allowedRoles) => {
|
|||||||
*/
|
*/
|
||||||
export const requireAdmin = requireRole('admin');
|
export const requireAdmin = requireRole('admin');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware pre admin alebo team_leader rolu
|
||||||
|
*/
|
||||||
|
export const requireTeamLeaderOrAdmin = requireRole('admin', 'team_leader');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as aiKurzyController from '../controllers/ai-kurzy.controller.js';
|
import * as aiKurzyController from '../controllers/ai-kurzy.controller.js';
|
||||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
import { requireTeamLeaderOrAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||||
import { validateBody, validateParams, validateQuery } from '../middlewares/security/validateInput.js';
|
import { validateBody, validateParams, validateQuery } from '../middlewares/security/validateInput.js';
|
||||||
import { createUpload, ALLOWED_FILE_TYPES } from '../config/upload.js';
|
import { createUpload, ALLOWED_FILE_TYPES } from '../config/upload.js';
|
||||||
import {
|
import {
|
||||||
@@ -29,9 +29,9 @@ const upload = createUpload({
|
|||||||
diskPath: path.join(process.cwd(), 'uploads', 'ai-kurzy'),
|
diskPath: path.join(process.cwd(), 'uploads', 'ai-kurzy'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// All routes require authentication and admin role
|
// All routes require authentication and admin or team_leader role
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
router.use(requireAdmin);
|
router.use(requireTeamLeaderOrAdmin);
|
||||||
|
|
||||||
// ==================== STATISTICS ====================
|
// ==================== STATISTICS ====================
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const eventIdSchema = z.object({
|
|||||||
// Všetky routes vyžadujú autentifikáciu
|
// Všetky routes vyžadujú autentifikáciu
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/events/today-count - Získať počet eventov a todos pre dnešok (pre badge v sidebar)
|
||||||
|
*/
|
||||||
|
router.get('/today-count', eventController.getTodayCount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/events - Získať eventy a todos podľa mesiaca (filtrované podľa assigned users)
|
* GET /api/events - Získať eventy a todos podľa mesiaca (filtrované podľa assigned users)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as serviceController from '../controllers/service.controller.js';
|
|||||||
import * as serviceFolderController from '../controllers/service-folder.controller.js';
|
import * as serviceFolderController from '../controllers/service-folder.controller.js';
|
||||||
import * as serviceDocumentController from '../controllers/service-document.controller.js';
|
import * as serviceDocumentController from '../controllers/service-document.controller.js';
|
||||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
import { requireAdmin, requireTeamLeaderOrAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||||
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
||||||
import { createServiceSchema, updateServiceSchema } from '../validators/crm.validators.js';
|
import { createServiceSchema, updateServiceSchema } from '../validators/crm.validators.js';
|
||||||
import { createUpload } from '../config/upload.js';
|
import { createUpload } from '../config/upload.js';
|
||||||
@@ -30,11 +30,11 @@ router.use(authenticate);
|
|||||||
router.get('/folders', serviceFolderController.getAllFolders);
|
router.get('/folders', serviceFolderController.getAllFolders);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/services/folders - Create new folder (admin only)
|
* POST /api/services/folders - Create new folder (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
'/folders',
|
'/folders',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateBody(createFolderSchema),
|
validateBody(createFolderSchema),
|
||||||
serviceFolderController.createFolder
|
serviceFolderController.createFolder
|
||||||
);
|
);
|
||||||
@@ -49,22 +49,22 @@ router.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/services/folders/:folderId - Update folder (admin only)
|
* PUT /api/services/folders/:folderId - Update folder (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.put(
|
router.put(
|
||||||
'/folders/:folderId',
|
'/folders/:folderId',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateParams(folderIdSchema),
|
validateParams(folderIdSchema),
|
||||||
validateBody(updateFolderSchema),
|
validateBody(updateFolderSchema),
|
||||||
serviceFolderController.updateFolder
|
serviceFolderController.updateFolder
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/services/folders/:folderId - Delete folder (admin only)
|
* DELETE /api/services/folders/:folderId - Delete folder (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.delete(
|
router.delete(
|
||||||
'/folders/:folderId',
|
'/folders/:folderId',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateParams(folderIdSchema),
|
validateParams(folderIdSchema),
|
||||||
serviceFolderController.deleteFolder
|
serviceFolderController.deleteFolder
|
||||||
);
|
);
|
||||||
@@ -100,11 +100,11 @@ router.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/services/folders/:folderId/documents/:documentId - Delete document (admin only)
|
* DELETE /api/services/folders/:folderId/documents/:documentId - Delete document (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.delete(
|
router.delete(
|
||||||
'/folders/:folderId/documents/:documentId',
|
'/folders/:folderId/documents/:documentId',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateParams(folderDocumentIdSchema),
|
validateParams(folderDocumentIdSchema),
|
||||||
serviceDocumentController.deleteDocument
|
serviceDocumentController.deleteDocument
|
||||||
);
|
);
|
||||||
@@ -117,11 +117,11 @@ router.delete(
|
|||||||
router.get('/', serviceController.getAllServices);
|
router.get('/', serviceController.getAllServices);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/services - Create new service (admin only)
|
* POST /api/services - Create new service (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateBody(createServiceSchema),
|
validateBody(createServiceSchema),
|
||||||
serviceController.createService
|
serviceController.createService
|
||||||
);
|
);
|
||||||
@@ -136,22 +136,22 @@ router.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/services/:serviceId - Update service (admin only)
|
* PUT /api/services/:serviceId - Update service (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.put(
|
router.put(
|
||||||
'/:serviceId',
|
'/:serviceId',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateParams(serviceIdSchema),
|
validateParams(serviceIdSchema),
|
||||||
validateBody(updateServiceSchema),
|
validateBody(updateServiceSchema),
|
||||||
serviceController.updateService
|
serviceController.updateService
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/services/:serviceId - Delete service (admin only)
|
* DELETE /api/services/:serviceId - Delete service (admin/team_leader)
|
||||||
*/
|
*/
|
||||||
router.delete(
|
router.delete(
|
||||||
'/:serviceId',
|
'/:serviceId',
|
||||||
requireAdmin,
|
requireTeamLeaderOrAdmin,
|
||||||
validateParams(serviceIdSchema),
|
validateParams(serviceIdSchema),
|
||||||
serviceController.deleteService
|
serviceController.deleteService
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import * as timeTrackingController from '../controllers/time-tracking.controller.js';
|
import * as timeTrackingController from '../controllers/time-tracking.controller.js';
|
||||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
import { requireTeamLeaderOrAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||||
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
||||||
import {
|
import {
|
||||||
startTimeEntrySchema,
|
startTimeEntrySchema,
|
||||||
@@ -47,8 +47,8 @@ router.post(
|
|||||||
// Get running time entry
|
// Get running time entry
|
||||||
router.get('/running', timeTrackingController.getRunningTimeEntry);
|
router.get('/running', timeTrackingController.getRunningTimeEntry);
|
||||||
|
|
||||||
// Get all running time entries (for dashboard) - admin only
|
// Get all running time entries (for dashboard) - admin/team_leader
|
||||||
router.get('/running-all', requireAdmin, timeTrackingController.getAllRunningTimeEntries);
|
router.get('/running-all', requireTeamLeaderOrAdmin, timeTrackingController.getAllRunningTimeEntries);
|
||||||
|
|
||||||
// Get all time entries with filters
|
// Get all time entries with filters
|
||||||
router.get('/', timeTrackingController.getAllTimeEntries);
|
router.get('/', timeTrackingController.getAllTimeEntries);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import * as timesheetController from '../controllers/timesheet.controller.js';
|
import * as timesheetController from '../controllers/timesheet.controller.js';
|
||||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
import { requireTeamLeaderOrAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||||
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { createUpload } from '../config/upload.js';
|
import { createUpload } from '../config/upload.js';
|
||||||
@@ -48,7 +48,7 @@ router.get('/my', timesheetController.getMyTimesheets);
|
|||||||
* Get all timesheets (admin only)
|
* Get all timesheets (admin only)
|
||||||
* GET /api/timesheets/all
|
* GET /api/timesheets/all
|
||||||
*/
|
*/
|
||||||
router.get('/all', requireAdmin, timesheetController.getAllTimesheets);
|
router.get('/all', requireTeamLeaderOrAdmin, timesheetController.getAllTimesheets);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download timesheet
|
* Download timesheet
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { users } from '../db/schema.js';
|
import { users } from '../db/schema.js';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
firstName: users.firstName,
|
firstName: users.firstName,
|
||||||
lastName: users.lastName,
|
lastName: users.lastName,
|
||||||
role: users.role,
|
role: users.role,
|
||||||
|
lastSeen: users.lastSeen,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.orderBy(users.username);
|
.orderBy(users.username);
|
||||||
@@ -31,4 +33,23 @@ router.get('/', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user's lastSeen timestamp (heartbeat)
|
||||||
|
* POST /api/users/heartbeat
|
||||||
|
*/
|
||||||
|
router.post('/heartbeat', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({ lastSeen: new Date() })
|
||||||
|
.where(eq(users.id, userId));
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export const getCombinedTableData = async () => {
|
|||||||
mesto: ucastnici.mesto,
|
mesto: ucastnici.mesto,
|
||||||
ulica: ucastnici.ulica,
|
ulica: ucastnici.ulica,
|
||||||
psc: ucastnici.psc,
|
psc: ucastnici.psc,
|
||||||
|
needsFollowup: ucastnici.needsFollowup,
|
||||||
kurzId: kurzy.id,
|
kurzId: kurzy.id,
|
||||||
kurzNazov: kurzy.nazov,
|
kurzNazov: kurzy.nazov,
|
||||||
kurzTyp: kurzy.typKurzu,
|
kurzTyp: kurzy.typKurzu,
|
||||||
@@ -155,7 +156,7 @@ export const getCombinedTableData = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateField = async (registrationId, field, value) => {
|
export const updateField = async (registrationId, field, value) => {
|
||||||
const ucastnikFields = ['titul', 'meno', 'priezvisko', 'email', 'telefon', 'firma', 'firmaIco', 'firmaDic', 'firmaIcDph', 'firmaSidlo', 'mesto', 'ulica', 'psc'];
|
const ucastnikFields = ['titul', 'meno', 'priezvisko', 'email', 'telefon', 'firma', 'firmaIco', 'firmaDic', 'firmaIcDph', 'firmaSidlo', 'mesto', 'ulica', 'psc', 'needsFollowup'];
|
||||||
const registraciaFields = ['datumOd', 'datumDo', 'formaKurzu', 'pocetUcastnikov', 'fakturaCislo', 'fakturaVystavena', 'zaplatene', 'stav', 'poznamka', 'kurzId'];
|
const registraciaFields = ['datumOd', 'datumDo', 'formaKurzu', 'pocetUcastnikov', 'fakturaCislo', 'fakturaVystavena', 'zaplatene', 'stav', 'poznamka', 'kurzId'];
|
||||||
const dateFields = ['datumOd', 'datumDo'];
|
const dateFields = ['datumOd', 'datumDo'];
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const getAllUcastnici = async () => {
|
|||||||
mesto: ucastnici.mesto,
|
mesto: ucastnici.mesto,
|
||||||
ulica: ucastnici.ulica,
|
ulica: ucastnici.ulica,
|
||||||
psc: ucastnici.psc,
|
psc: ucastnici.psc,
|
||||||
|
needsFollowup: ucastnici.needsFollowup,
|
||||||
createdAt: ucastnici.createdAt,
|
createdAt: ucastnici.createdAt,
|
||||||
registraciiCount: sql`(SELECT COUNT(*) FROM registracie WHERE ucastnik_id = ${ucastnici.id})::int`,
|
registraciiCount: sql`(SELECT COUNT(*) FROM registracie WHERE ucastnik_id = ${ucastnici.id})::int`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { companies, projects, todos, notes, companyReminders, companyUsers, users } from '../db/schema.js';
|
import { companies, projects, todos, notes, companyReminders, companyUsers, users, personalContacts } from '../db/schema.js';
|
||||||
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
|
import { eq, desc, ilike, or, and, inArray, sql } from 'drizzle-orm';
|
||||||
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
||||||
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
|
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
|
||||||
import {
|
import {
|
||||||
@@ -57,11 +57,31 @@ export const getAllCompanies = async (searchTerm = null, userId = null, userRole
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
|
// Find company IDs that have matching contacts
|
||||||
|
const contactMatches = await db
|
||||||
|
.selectDistinct({ companyId: personalContacts.companyId })
|
||||||
|
.from(personalContacts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
sql`${personalContacts.companyId} IS NOT NULL`,
|
||||||
|
or(
|
||||||
|
ilike(personalContacts.firstName, `%${searchTerm}%`),
|
||||||
|
ilike(personalContacts.lastName, `%${searchTerm}%`),
|
||||||
|
ilike(personalContacts.email, `%${searchTerm}%`),
|
||||||
|
ilike(personalContacts.phone, `%${searchTerm}%`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const contactCompanyIds = contactMatches.map((c) => c.companyId).filter(Boolean);
|
||||||
|
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(
|
||||||
ilike(companies.name, `%${searchTerm}%`),
|
ilike(companies.name, `%${searchTerm}%`),
|
||||||
ilike(companies.email, `%${searchTerm}%`),
|
ilike(companies.email, `%${searchTerm}%`),
|
||||||
ilike(companies.city, `%${searchTerm}%`)
|
ilike(companies.city, `%${searchTerm}%`),
|
||||||
|
ilike(companies.phone, `%${searchTerm}%`),
|
||||||
|
contactCompanyIds.length > 0 ? inArray(companies.id, contactCompanyIds) : sql`false`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { events, eventUsers, users, todos, todoUsers } from '../db/schema.js';
|
import { events, eventUsers, users, todos, todoUsers, companyReminders, companyUsers, companies } from '../db/schema.js';
|
||||||
import { eq, and, gte, lt, desc, inArray } from 'drizzle-orm';
|
import { eq, and, gte, lt, desc, inArray, sql, ne } from 'drizzle-orm';
|
||||||
import { NotFoundError } from '../utils/errors.js';
|
import { NotFoundError } from '../utils/errors.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,9 +202,56 @@ export const getCalendarData = async (year, month, userId, isAdmin) => {
|
|||||||
updatedAt: todo.updatedAt instanceof Date ? todo.updatedAt.toISOString() : todo.updatedAt,
|
updatedAt: todo.updatedAt instanceof Date ? todo.updatedAt.toISOString() : todo.updatedAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Get company reminders for month
|
||||||
|
let accessibleCompanyIds = null;
|
||||||
|
if (!isAdmin) {
|
||||||
|
// Member sees only reminders from companies they are assigned to
|
||||||
|
const userCompanies = await db
|
||||||
|
.select({ companyId: companyUsers.companyId })
|
||||||
|
.from(companyUsers)
|
||||||
|
.where(eq(companyUsers.userId, userId));
|
||||||
|
accessibleCompanyIds = userCompanies.map((row) => row.companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let monthReminders = [];
|
||||||
|
if (isAdmin || (accessibleCompanyIds && accessibleCompanyIds.length > 0)) {
|
||||||
|
const reminderConditions = [
|
||||||
|
gte(companyReminders.dueDate, startOfMonth),
|
||||||
|
lt(companyReminders.dueDate, endOfMonth),
|
||||||
|
eq(companyReminders.isChecked, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isAdmin && accessibleCompanyIds) {
|
||||||
|
reminderConditions.push(inArray(companyReminders.companyId, accessibleCompanyIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
monthReminders = await db
|
||||||
|
.select({
|
||||||
|
id: companyReminders.id,
|
||||||
|
companyId: companyReminders.companyId,
|
||||||
|
description: companyReminders.description,
|
||||||
|
dueDate: companyReminders.dueDate,
|
||||||
|
isChecked: companyReminders.isChecked,
|
||||||
|
createdAt: companyReminders.createdAt,
|
||||||
|
companyName: companies.name,
|
||||||
|
})
|
||||||
|
.from(companyReminders)
|
||||||
|
.innerJoin(companies, eq(companyReminders.companyId, companies.id))
|
||||||
|
.where(and(...reminderConditions))
|
||||||
|
.orderBy(desc(companyReminders.dueDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format reminders for calendar
|
||||||
|
const formattedReminders = monthReminders.map((reminder) => ({
|
||||||
|
...reminder,
|
||||||
|
dueDate: reminder.dueDate instanceof Date ? reminder.dueDate.toISOString() : reminder.dueDate,
|
||||||
|
createdAt: reminder.createdAt instanceof Date ? reminder.createdAt.toISOString() : reminder.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events: formattedEvents,
|
events: formattedEvents,
|
||||||
todos: formattedTodos,
|
todos: formattedTodos,
|
||||||
|
reminders: formattedReminders,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -352,3 +399,91 @@ export const deleteEvent = async (eventId) => {
|
|||||||
|
|
||||||
return { success: true, message: 'Event bol zmazaný' };
|
return { success: true, message: 'Event bol zmazaný' };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of events and todos for today
|
||||||
|
* Used for calendar badge in sidebar
|
||||||
|
*/
|
||||||
|
export const getTodayCalendarCount = async (userId, isAdmin) => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||||
|
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||||
|
|
||||||
|
// Get event count for today
|
||||||
|
let eventCount;
|
||||||
|
if (isAdmin) {
|
||||||
|
const eventResult = await db
|
||||||
|
.select({ count: sql`count(*)::int` })
|
||||||
|
.from(events)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(events.start, startOfDay),
|
||||||
|
lt(events.start, endOfDay)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
eventCount = eventResult[0]?.count || 0;
|
||||||
|
} else {
|
||||||
|
const accessibleEventIds = await getAccessibleEventIds(userId);
|
||||||
|
if (accessibleEventIds.length === 0) {
|
||||||
|
eventCount = 0;
|
||||||
|
} else {
|
||||||
|
const eventResult = await db
|
||||||
|
.select({ count: sql`count(*)::int` })
|
||||||
|
.from(events)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(events.start, startOfDay),
|
||||||
|
lt(events.start, endOfDay),
|
||||||
|
inArray(events.id, accessibleEventIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
eventCount = eventResult[0]?.count || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get todo count for today
|
||||||
|
let todoCount;
|
||||||
|
if (isAdmin) {
|
||||||
|
const todoResult = await db
|
||||||
|
.select({ count: sql`count(*)::int` })
|
||||||
|
.from(todos)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(todos.dueDate, startOfDay),
|
||||||
|
lt(todos.dueDate, endOfDay),
|
||||||
|
ne(todos.status, 'completed')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
todoCount = todoResult[0]?.count || 0;
|
||||||
|
} else {
|
||||||
|
const userTodos = await db
|
||||||
|
.select({ todoId: todoUsers.todoId })
|
||||||
|
.from(todoUsers)
|
||||||
|
.where(eq(todoUsers.userId, userId));
|
||||||
|
|
||||||
|
const todoIds = userTodos.map((row) => row.todoId);
|
||||||
|
|
||||||
|
if (todoIds.length === 0) {
|
||||||
|
todoCount = 0;
|
||||||
|
} else {
|
||||||
|
const todoResult = await db
|
||||||
|
.select({ count: sql`count(*)::int` })
|
||||||
|
.from(todos)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(todos.dueDate, startOfDay),
|
||||||
|
lt(todos.dueDate, endOfDay),
|
||||||
|
ne(todos.status, 'completed'),
|
||||||
|
inArray(todos.id, todoIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
todoCount = todoResult[0]?.count || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventCount,
|
||||||
|
todoCount,
|
||||||
|
totalCount: eventCount + todoCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export const getChatUsers = async (currentUserId) => {
|
|||||||
lastName: users.lastName,
|
lastName: users.lastName,
|
||||||
role: users.role,
|
role: users.role,
|
||||||
lastLogin: users.lastLogin,
|
lastLogin: users.lastLogin,
|
||||||
|
lastSeen: users.lastSeen,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(ne(users.id, currentUserId))
|
.where(ne(users.id, currentUserId))
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
export const getAllTodos = async (filters = {}, userId = null, userRole = null) => {
|
export const getAllTodos = async (filters = {}, userId = null, userRole = null) => {
|
||||||
const { searchTerm, projectId, companyId, assignedTo, status, priority } = filters;
|
const { searchTerm, projectId, companyId, assignedTo, status, priority } = filters;
|
||||||
|
|
||||||
// Pre membera filtruj len todos kde je priradeny
|
// Pre membera filtruj len todos kde je priradeny (admin a team_leader vidia vsetko)
|
||||||
let accessibleTodoIds = null;
|
let accessibleTodoIds = null;
|
||||||
if (userRole && userRole !== 'admin' && userId) {
|
if (userRole && userRole !== 'admin' && userRole !== 'team_leader' && userId) {
|
||||||
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
|
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
|
||||||
// Ak member nema pristup k ziadnym todos, vrat prazdne pole
|
// Ak member nema pristup k ziadnym todos, vrat prazdne pole
|
||||||
if (accessibleTodoIds.length === 0) {
|
if (accessibleTodoIds.length === 0) {
|
||||||
@@ -457,9 +457,9 @@ export const getTodosByUserId = async (userId) => {
|
|||||||
export const getOverdueCount = async (userId, userRole) => {
|
export const getOverdueCount = async (userId, userRole) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Get accessible todo IDs for non-admin users
|
// Get accessible todo IDs for non-admin/non-team_leader users
|
||||||
let accessibleTodoIds = null;
|
let accessibleTodoIds = null;
|
||||||
if (userRole && userRole !== 'admin') {
|
if (userRole && userRole !== 'admin' && userRole !== 'team_leader') {
|
||||||
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
|
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
|
||||||
if (accessibleTodoIds.length === 0) {
|
if (accessibleTodoIds.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const createUserSchema = z.object({
|
|||||||
emailPassword: z.string().min(1).optional(),
|
emailPassword: z.string().min(1).optional(),
|
||||||
firstName: z.string().max(100).optional(),
|
firstName: z.string().max(100).optional(),
|
||||||
lastName: z.string().max(100).optional(),
|
lastName: z.string().max(100).optional(),
|
||||||
role: z.enum(['admin', 'member']).optional(),
|
role: z.enum(['admin', 'team_leader', 'member']).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update user schema
|
// Update user schema
|
||||||
@@ -89,7 +89,7 @@ export const updateUserSchema = z.object({
|
|||||||
// Change role schema (admin only)
|
// Change role schema (admin only)
|
||||||
export const changeRoleSchema = z.object({
|
export const changeRoleSchema = z.object({
|
||||||
userId: z.string().uuid('Neplatný formát user ID'),
|
userId: z.string().uuid('Neplatný formát user ID'),
|
||||||
role: z.enum(['admin', 'member'], {
|
role: z.enum(['admin', 'team_leader', 'member'], {
|
||||||
required_error: 'Rola je povinná',
|
required_error: 'Rola je povinná',
|
||||||
invalid_type_error: 'Neplatná rola',
|
invalid_type_error: 'Neplatná rola',
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user