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

@@ -1,6 +1,6 @@
import { db } from '../config/database.js';
import { todos, todoUsers, notes, projects, companies, users } from '../db/schema.js';
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { eq, desc, ilike, or, and, inArray, lt, ne, sql } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { sendPushNotificationToUsers } from './push.service.js';
@@ -471,3 +471,90 @@ export const getTodosByUserId = async (userId) => {
.where(inArray(todos.id, todoIds))
.orderBy(desc(todos.createdAt));
};
/**
* Get count of overdue todos for a user
* Overdue = dueDate < now AND status !== 'completed'
* For members: only counts todos they are assigned to
*/
export const getOverdueCount = async (userId, userRole) => {
const now = new Date();
// Get accessible todo IDs for non-admin users
let accessibleTodoIds = null;
if (userRole && userRole !== 'admin') {
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
if (accessibleTodoIds.length === 0) {
return 0;
}
}
const conditions = [
lt(todos.dueDate, now),
ne(todos.status, 'completed'),
];
if (accessibleTodoIds !== null) {
conditions.push(inArray(todos.id, accessibleTodoIds));
}
const result = await db
.select({ count: sql`count(*)::int` })
.from(todos)
.where(and(...conditions));
return result[0]?.count || 0;
};
/**
* Get count of todos created by user that were completed but not yet notified
* Returns todos where createdBy = userId AND status = 'completed' AND completedNotifiedAt IS NULL
*/
export const getCompletedByMeCount = async (userId) => {
const result = await db
.select({ count: sql`count(*)::int` })
.from(todos)
.where(
and(
eq(todos.createdBy, userId),
eq(todos.status, 'completed'),
sql`${todos.completedNotifiedAt} IS NULL`
)
);
return result[0]?.count || 0;
};
/**
* Mark all completed todos created by user as notified
* Called when user opens the Todos page
*/
export const markCompletedAsNotified = async (userId) => {
await db
.update(todos)
.set({ completedNotifiedAt: new Date() })
.where(
and(
eq(todos.createdBy, userId),
eq(todos.status, 'completed'),
sql`${todos.completedNotifiedAt} IS NULL`
)
);
return { success: true };
};
/**
* Get combined todo counts for sidebar badges
*/
export const getTodoCounts = async (userId, userRole) => {
const [overdueCount, completedByMeCount] = await Promise.all([
getOverdueCount(userId, userRole),
getCompletedByMeCount(userId),
]);
return {
overdueCount,
completedByMeCount,
};
};