feat: Group chat and push notifications

- Add group chat tables (chat_groups, chat_group_members, group_messages)
- Add push subscriptions table for web push notifications
- Add group service, controller, routes
- Add push service, controller, routes
- Integrate push notifications with todos, messages, group messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-20 07:27:13 +01:00
parent 73a3c6bf95
commit d9f16ad0a6
15 changed files with 1233 additions and 4 deletions

View File

@@ -3,6 +3,8 @@ import { todos, todoUsers, notes, projects, companies, users } from '../db/schem
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { sendPushNotificationToUsers } from './push.service.js';
import { logger } from '../utils/logger.js';
/**
* Get all todos
@@ -217,6 +219,25 @@ export const createTodo = async (userId, data) => {
});
}
// Send push notifications to assigned users (excluding creator)
if (assignedUserIds && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
try {
await sendPushNotificationToUsers(
assignedUserIds,
{
title: 'Nová úloha',
body: `Bola vám priradená úloha: ${title}`,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: '/todos', todoId: newTodo.id },
},
userId // exclude creator
);
} catch (error) {
logger.error('Failed to send push notifications for new todo', error);
}
}
return newTodo;
};
@@ -224,8 +245,9 @@ export const createTodo = async (userId, data) => {
* Update todo
* @param {string} todoId - ID of todo to update
* @param {object} data - Updated data including assignedUserIds array
* @param {string} updatedByUserId - ID of user making the update (for notifications)
*/
export const updateTodo = async (todoId, data) => {
export const updateTodo = async (todoId, data, updatedByUserId = null) => {
const todo = await getTodoById(todoId);
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
@@ -294,6 +316,13 @@ export const updateTodo = async (todoId, data) => {
// Update assigned users if provided
if (assignedUserIds !== undefined) {
// Get existing assigned users before deleting
const existingAssignments = await db
.select({ userId: todoUsers.userId })
.from(todoUsers)
.where(eq(todoUsers.todoId, todoId));
const existingUserIds = existingAssignments.map(a => a.userId);
// Delete existing assignments
await db.delete(todoUsers).where(eq(todoUsers.todoId, todoId));
@@ -302,10 +331,34 @@ export const updateTodo = async (todoId, data) => {
const todoUserInserts = assignedUserIds.map((userId) => ({
todoId: todoId,
userId: userId,
assignedBy: null, // We don't track who made the update
assignedBy: updatedByUserId,
}));
await db.insert(todoUsers).values(todoUserInserts);
// Find newly assigned users (not in existing list)
const newlyAssignedUserIds = assignedUserIds.filter(
id => !existingUserIds.includes(id)
);
// Send push notifications to newly assigned users
if (newlyAssignedUserIds.length > 0) {
try {
await sendPushNotificationToUsers(
newlyAssignedUserIds,
{
title: 'Priradená úloha',
body: `Bola vám priradená úloha: ${updated.title}`,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: '/todos', todoId: todoId },
},
updatedByUserId // exclude user making the change
);
} catch (error) {
logger.error('Failed to send push notifications for updated todo', error);
}
}
}
}