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:
richardtekula
2026-01-28 17:23:57 +01:00
parent c3c42ec1e4
commit a4a81ef88e
16 changed files with 246 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
import { db } from '../config/database.js';
import { events, eventUsers, users, todos, todoUsers } from '../db/schema.js';
import { eq, and, gte, lt, desc, inArray } from 'drizzle-orm';
import { events, eventUsers, users, todos, todoUsers, companyReminders, companyUsers, companies } from '../db/schema.js';
import { eq, and, gte, lt, desc, inArray, sql, ne } from 'drizzle-orm';
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,
}));
// 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 {
events: formattedEvents,
todos: formattedTodos,
reminders: formattedReminders,
};
};
@@ -352,3 +399,91 @@ export const deleteEvent = async (eventId) => {
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,
};
};