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:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user