Files
crm-server/src/services/group.service.js
richardtekula 4089bb4be2 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>
2026-01-21 11:32:49 +01:00

473 lines
12 KiB
JavaScript

import { db } from '../config/database.js';
import { chatGroups, chatGroupMembers, groupMessages, users } from '../db/schema.js';
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';
/**
* Create a new group chat
*/
export const createGroup = async (name, creatorId, memberIds) => {
const [group] = await db
.insert(chatGroups)
.values({
name: name.trim(),
createdById: creatorId,
})
.returning();
// Add creator and all members (ensure unique)
const allMemberIds = [...new Set([creatorId, ...memberIds])];
await db.insert(chatGroupMembers).values(
allMemberIds.map((userId) => ({
groupId: group.id,
userId,
}))
);
return group;
};
/**
* Get all groups for a user
*/
export const getUserGroups = async (userId) => {
// Get groups where user is a member 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 [];
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({
id: chatGroups.id,
name: chatGroups.name,
createdById: chatGroups.createdById,
createdAt: chatGroups.createdAt,
updatedAt: chatGroups.updatedAt,
})
.from(chatGroups)
.where(inArray(chatGroups.id, groupIds))
.orderBy(desc(chatGroups.updatedAt));
// Get last message, member count, and unread count for each group
const result = await Promise.all(
groups.map(async (group) => {
const lastReadAt = lastReadMap[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,
lastMessage: lastMessage[0] ? {
content: lastMessage[0].content,
createdAt: lastMessage[0].createdAt,
senderName: lastMessage[0].senderFirstName || lastMessage[0].senderUsername,
isMine: lastMessage[0].senderId === userId,
} : null,
memberCount: members.length,
unreadCount: unreadResult[0]?.count || 0,
type: 'group',
};
})
);
return result;
};
/**
* Get group details with members
*/
export const getGroupDetails = async (groupId, userId) => {
// Verify user is member
const [isMember] = await db
.select()
.from(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, userId)
)
)
.limit(1);
if (!isMember) {
throw new ForbiddenError('Nie ste členom tejto skupiny');
}
const [group] = await db
.select()
.from(chatGroups)
.where(eq(chatGroups.id, groupId))
.limit(1);
if (!group) {
throw new NotFoundError('Skupina nenájdená');
}
const members = await db
.select({
id: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
})
.from(chatGroupMembers)
.innerJoin(users, eq(chatGroupMembers.userId, users.id))
.where(eq(chatGroupMembers.groupId, groupId));
return {
...group,
members,
};
};
/**
* Get messages for a group
*/
export const getGroupMessages = async (groupId, userId) => {
// Verify user is member
const [isMember] = await db
.select()
.from(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, userId)
)
)
.limit(1);
if (!isMember) {
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,
content: groupMessages.content,
createdAt: groupMessages.createdAt,
senderId: groupMessages.senderId,
senderUsername: users.username,
senderFirstName: users.firstName,
senderLastName: users.lastName,
})
.from(groupMessages)
.leftJoin(users, eq(groupMessages.senderId, users.id))
.where(eq(groupMessages.groupId, groupId))
.orderBy(groupMessages.createdAt);
return messages.map((msg) => ({
id: msg.id,
content: msg.content,
createdAt: msg.createdAt,
senderId: msg.senderId,
senderName: msg.senderFirstName || msg.senderUsername || 'Neznámy',
isMine: msg.senderId === userId,
}));
};
/**
* Send message to group
*/
export const sendGroupMessage = async (groupId, senderId, content) => {
// Verify user is member
const [isMember] = await db
.select()
.from(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, senderId)
)
)
.limit(1);
if (!isMember) {
throw new ForbiddenError('Nie ste členom tejto skupiny');
}
const [message] = await db
.insert(groupMessages)
.values({
groupId,
senderId,
content: content.trim(),
})
.returning();
// Update group's updatedAt
await db
.update(chatGroups)
.set({ updatedAt: new Date() })
.where(eq(chatGroups.id, groupId));
// Send push notifications to all group members (except sender)
try {
const [group] = await db
.select({ name: chatGroups.name })
.from(chatGroups)
.where(eq(chatGroups.id, groupId))
.limit(1);
const [sender] = await db
.select({ firstName: users.firstName, username: users.username })
.from(users)
.where(eq(users.id, senderId))
.limit(1);
const members = await db
.select({ userId: chatGroupMembers.userId })
.from(chatGroupMembers)
.where(eq(chatGroupMembers.groupId, groupId));
const memberIds = members.map(m => m.userId);
const senderName = sender?.firstName || sender?.username || 'Niekto';
const groupName = group?.name || 'Skupina';
await sendPushNotificationToUsers(
memberIds,
{
title: `${groupName}`,
body: `${senderName}: ${content.trim().substring(0, 80)}${content.length > 80 ? '...' : ''}`,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: `/chat/group/${groupId}` },
},
senderId // exclude sender
);
} catch (error) {
logger.error('Failed to send push notifications for group message', error);
}
return {
id: message.id,
content: message.content,
createdAt: message.createdAt,
senderId: message.senderId,
isMine: true,
};
};
/**
* Add member to group
*/
export const addGroupMember = async (groupId, userId, requesterId) => {
// Verify requester is member
const [isMember] = await db
.select()
.from(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, requesterId)
)
)
.limit(1);
if (!isMember) {
throw new ForbiddenError('Nie ste členom tejto skupiny');
}
// Check if already member
const [alreadyMember] = await db
.select()
.from(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, userId)
)
)
.limit(1);
if (alreadyMember) {
throw new Error('Používateľ je už členom skupiny');
}
// Verify user exists
const [userExists] = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!userExists) {
throw new NotFoundError('Používateľ nenájdený');
}
await db.insert(chatGroupMembers).values({ groupId, userId });
return { success: true };
};
/**
* Remove member from group (or leave)
*/
export const removeGroupMember = async (groupId, userId, requesterId) => {
// User can remove themselves
if (userId !== requesterId) {
// Check if requester is the creator
const [group] = await db
.select({ createdById: chatGroups.createdById })
.from(chatGroups)
.where(eq(chatGroups.id, groupId))
.limit(1);
if (!group || group.createdById !== requesterId) {
throw new ForbiddenError('Nemáte oprávnenie odstrániť tohto člena');
}
}
await db
.delete(chatGroupMembers)
.where(
and(
eq(chatGroupMembers.groupId, groupId),
eq(chatGroupMembers.userId, userId)
)
);
return { success: true };
};
/**
* Update group name
*/
export const updateGroupName = async (groupId, name, requesterId) => {
// Verify requester is the creator
const [group] = await db
.select({ createdById: chatGroups.createdById })
.from(chatGroups)
.where(eq(chatGroups.id, groupId))
.limit(1);
if (!group) {
throw new NotFoundError('Skupina nenájdená');
}
if (group.createdById !== requesterId) {
throw new ForbiddenError('Nemáte oprávnenie upraviť názov skupiny');
}
const [updated] = await db
.update(chatGroups)
.set({ name: name.trim(), updatedAt: new Date() })
.where(eq(chatGroups.id, groupId))
.returning();
return updated;
};
/**
* Delete group (only creator can delete)
*/
export const deleteGroup = async (groupId, requesterId) => {
const [group] = await db
.select({ createdById: chatGroups.createdById })
.from(chatGroups)
.where(eq(chatGroups.id, groupId))
.limit(1);
if (!group) {
throw new NotFoundError('Skupina nenájdená');
}
if (group.createdById !== requesterId) {
throw new ForbiddenError('Nemáte oprávnenie odstrániť túto skupinu');
}
await db.delete(chatGroups).where(eq(chatGroups.id, groupId));
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;
};