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 { chatGroups, chatGroupMembers, groupMessages, users } from '../db/schema.js';
import { eq, and, desc, inArray, sql } from 'drizzle-orm';
import { eq, and, desc, inArray, sql, gt } from 'drizzle-orm';
import { NotFoundError, ForbiddenError } from '../utils/errors.js';
import { sendPushNotificationToUsers } from './push.service.js';
import { logger } from '../utils/logger.js';
@@ -34,15 +34,22 @@ export const createGroup = async (name, creatorId, memberIds) => {
* Get all groups for a user
*/
export const getUserGroups = async (userId) => {
// Get groups where user is a member
// Get groups where user is a member with lastReadAt
const memberOf = await db
.select({ groupId: chatGroupMembers.groupId })
.select({
groupId: chatGroupMembers.groupId,
lastReadAt: chatGroupMembers.lastReadAt,
})
.from(chatGroupMembers)
.where(eq(chatGroupMembers.userId, userId));
if (memberOf.length === 0) return [];
const groupIds = memberOf.map((m) => m.groupId);
const lastReadMap = memberOf.reduce((acc, m) => {
acc[m.groupId] = m.lastReadAt;
return acc;
}, {});
const groups = await db
.select({
@@ -56,27 +63,41 @@ export const getUserGroups = async (userId) => {
.where(inArray(chatGroups.id, groupIds))
.orderBy(desc(chatGroups.updatedAt));
// Get last message and member count for each group
// Get last message, member count, and unread count for each group
const result = await Promise.all(
groups.map(async (group) => {
const lastMessage = await db
.select({
content: groupMessages.content,
createdAt: groupMessages.createdAt,
senderId: groupMessages.senderId,
senderFirstName: users.firstName,
senderUsername: users.username,
})
.from(groupMessages)
.leftJoin(users, eq(groupMessages.senderId, users.id))
.where(eq(groupMessages.groupId, group.id))
.orderBy(desc(groupMessages.createdAt))
.limit(1);
const lastReadAt = lastReadMap[group.id];
const members = await db
.select({ id: chatGroupMembers.id })
.from(chatGroupMembers)
.where(eq(chatGroupMembers.groupId, group.id));
const [lastMessage, members, unreadResult] = await Promise.all([
db
.select({
content: groupMessages.content,
createdAt: groupMessages.createdAt,
senderId: groupMessages.senderId,
senderFirstName: users.firstName,
senderUsername: users.username,
})
.from(groupMessages)
.leftJoin(users, eq(groupMessages.senderId, users.id))
.where(eq(groupMessages.groupId, group.id))
.orderBy(desc(groupMessages.createdAt))
.limit(1),
db
.select({ id: chatGroupMembers.id })
.from(chatGroupMembers)
.where(eq(chatGroupMembers.groupId, group.id)),
// Count unread messages (messages after lastReadAt, not sent by current user)
db
.select({ count: sql`count(*)::int` })
.from(groupMessages)
.where(
and(
eq(groupMessages.groupId, group.id),
gt(groupMessages.createdAt, lastReadAt),
sql`${groupMessages.senderId} != ${userId}`
)
),
]);
return {
...group,
@@ -87,6 +108,7 @@ export const getUserGroups = async (userId) => {
isMine: lastMessage[0].senderId === userId,
} : null,
memberCount: members.length,
unreadCount: unreadResult[0]?.count || 0,
type: 'group',
};
})
@@ -163,6 +185,17 @@ export const getGroupMessages = async (groupId, userId) => {
throw new ForbiddenError('Nie ste členom tejto skupiny');
}
// Update lastReadAt for this user in this group
await db
.update(chatGroupMembers)
.set({ lastReadAt: new Date() })
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, userId)
)
);
const messages = await db
.select({
id: groupMessages.id,
@@ -402,3 +435,38 @@ export const deleteGroup = async (groupId, requesterId) => {
return { success: true };
};
/**
* Get total unread group messages count for a user
*/
export const getGroupUnreadCount = async (userId) => {
// Get all groups user is a member of with lastReadAt
const memberOf = await db
.select({
groupId: chatGroupMembers.groupId,
lastReadAt: chatGroupMembers.lastReadAt,
})
.from(chatGroupMembers)
.where(eq(chatGroupMembers.userId, userId));
if (memberOf.length === 0) return 0;
// Count unread messages across all groups
let totalUnread = 0;
for (const membership of memberOf) {
const result = await db
.select({ count: sql`count(*)::int` })
.from(groupMessages)
.where(
and(
eq(groupMessages.groupId, membership.groupId),
gt(groupMessages.createdAt, membership.lastReadAt),
sql`${groupMessages.senderId} != ${userId}`
)
);
totalUnread += result[0]?.count || 0;
}
return totalUnread;
};