Implement many-to-many TODO user assignments

- Create todo_users junction table for many-to-many relationship
- Add migration to create todo_users table and migrate existing data
- Update validators to accept assignedUserIds array instead of assignedTo
- Update todo service to handle multiple user assignments
- Fetch and return assigned users with each TODO

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-11-24 11:17:28 +01:00
parent 7fd6b9e742
commit 8fd8f991e8
5 changed files with 248 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
import { db } from '../config/database.js';
import { todos, notes, projects, companies, users } from '../db/schema.js';
import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { todos, todoUsers, notes, projects, companies, users } from '../db/schema.js';
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
/**
@@ -10,6 +10,90 @@ import { NotFoundError } from '../utils/errors.js';
export const getAllTodos = async (filters = {}) => {
const { searchTerm, projectId, companyId, assignedTo, status } = filters;
// If filtering by assignedTo, we need to join with todo_users
if (assignedTo) {
const todoIdsWithUser = await db
.select({ todoId: todoUsers.todoId })
.from(todoUsers)
.where(eq(todoUsers.userId, assignedTo));
const todoIds = todoIdsWithUser.map((row) => row.todoId);
if (todoIds.length === 0) {
return [];
}
let query = db.select().from(todos).where(inArray(todos.id, todoIds));
const conditions = [];
if (searchTerm) {
conditions.push(
or(
ilike(todos.title, `%${searchTerm}%`),
ilike(todos.description, `%${searchTerm}%`)
)
);
}
if (projectId) {
conditions.push(eq(todos.projectId, projectId));
}
if (companyId) {
conditions.push(eq(todos.companyId, companyId));
}
if (status) {
conditions.push(eq(todos.status, status));
}
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
const result = await query.orderBy(desc(todos.createdAt));
// Fetch assigned users for all todos
if (result.length > 0) {
const todoIds = result.map(todo => todo.id);
const assignedUsersData = await db
.select({
todoId: todoUsers.todoId,
userId: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(todoUsers)
.innerJoin(users, eq(todoUsers.userId, users.id))
.where(inArray(todoUsers.todoId, todoIds));
// Group assigned users by todoId
const usersByTodoId = {};
for (const row of assignedUsersData) {
if (!usersByTodoId[row.todoId]) {
usersByTodoId[row.todoId] = [];
}
usersByTodoId[row.todoId].push({
id: row.userId,
username: row.username,
firstName: row.firstName,
lastName: row.lastName,
});
}
// Attach assigned users to each todo
return result.map(todo => ({
...todo,
assignedUsers: usersByTodoId[todo.id] || [],
}));
}
return result;
}
// No assignedTo filter - simple query
let query = db.select().from(todos);
const conditions = [];
@@ -31,10 +115,6 @@ export const getAllTodos = async (filters = {}) => {
conditions.push(eq(todos.companyId, companyId));
}
if (assignedTo) {
conditions.push(eq(todos.assignedTo, assignedTo));
}
if (status) {
conditions.push(eq(todos.status, status));
}
@@ -44,6 +124,43 @@ export const getAllTodos = async (filters = {}) => {
}
const result = await query.orderBy(desc(todos.createdAt));
// Fetch assigned users for all todos
if (result.length > 0) {
const todoIds = result.map(todo => todo.id);
const assignedUsersData = await db
.select({
todoId: todoUsers.todoId,
userId: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(todoUsers)
.innerJoin(users, eq(todoUsers.userId, users.id))
.where(inArray(todoUsers.todoId, todoIds));
// Group assigned users by todoId
const usersByTodoId = {};
for (const row of assignedUsersData) {
if (!usersByTodoId[row.todoId]) {
usersByTodoId[row.todoId] = [];
}
usersByTodoId[row.todoId].push({
id: row.userId,
username: row.username,
firstName: row.firstName,
lastName: row.lastName,
});
}
// Attach assigned users to each todo
return result.map(todo => ({
...todo,
assignedUsers: usersByTodoId[todo.id] || [],
}));
}
return result;
};
@@ -66,9 +183,12 @@ export const getTodoById = async (todoId) => {
/**
* Create new todo
* @param {string} userId - ID of user creating the todo
* @param {object} data - Todo data including assignedUserIds array
*/
export const createTodo = async (userId, data) => {
const { title, description, projectId, companyId, assignedTo, status, priority, dueDate } = data;
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
console.log('Service createTodo - assignedUserIds:', assignedUserIds);
// Verify project exists if provided
if (projectId) {
@@ -96,19 +216,19 @@ export const createTodo = async (userId, data) => {
}
}
// Verify assigned user exists if provided
if (assignedTo) {
const [user] = await db
.select()
// Verify assigned users exist if provided
if (assignedUserIds && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
const existingUsers = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, assignedTo))
.limit(1);
.where(inArray(users.id, assignedUserIds));
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
if (existingUsers.length !== assignedUserIds.length) {
throw new NotFoundError('Niektorí používatelia neboli nájdení');
}
}
// Create the todo
const [newTodo] = await db
.insert(todos)
.values({
@@ -116,7 +236,6 @@ export const createTodo = async (userId, data) => {
description: description || null,
projectId: projectId || null,
companyId: companyId || null,
assignedTo: assignedTo || null,
status: status || 'pending',
priority: priority || 'medium',
dueDate: dueDate ? new Date(dueDate) : null,
@@ -124,16 +243,32 @@ export const createTodo = async (userId, data) => {
})
.returning();
// Assign users to the todo
if (assignedUserIds && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
const todoUserInserts = assignedUserIds.map((assignedUserId) => ({
todoId: newTodo.id,
userId: assignedUserId,
assignedBy: userId,
}));
console.log('Inserting todo_users:', todoUserInserts);
await db.insert(todoUsers).values(todoUserInserts);
} else {
console.log('No users to assign - assignedUserIds:', assignedUserIds);
}
return newTodo;
};
/**
* Update todo
* @param {string} todoId - ID of todo to update
* @param {object} data - Updated data including assignedUserIds array
*/
export const updateTodo = async (todoId, data) => {
const todo = await getTodoById(todoId);
const { title, description, projectId, companyId, assignedTo, status, priority, dueDate } = data;
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
// Verify project exists if being changed
if (projectId !== undefined && projectId !== null && projectId !== todo.projectId) {
@@ -161,16 +296,15 @@ export const updateTodo = async (todoId, data) => {
}
}
// Verify assigned user exists if being changed
if (assignedTo !== undefined && assignedTo !== null && assignedTo !== todo.assignedTo) {
const [user] = await db
.select()
// Verify assigned users exist if being changed
if (assignedUserIds !== undefined && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
const existingUsers = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, assignedTo))
.limit(1);
.where(inArray(users.id, assignedUserIds));
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
if (existingUsers.length !== assignedUserIds.length) {
throw new NotFoundError('Niektorí používatelia neboli nájdení');
}
}
@@ -189,7 +323,6 @@ export const updateTodo = async (todoId, data) => {
description: description !== undefined ? description : todo.description,
projectId: projectId !== undefined ? projectId : todo.projectId,
companyId: companyId !== undefined ? companyId : todo.companyId,
assignedTo: assignedTo !== undefined ? assignedTo : todo.assignedTo,
status: status !== undefined ? status : todo.status,
priority: priority !== undefined ? priority : todo.priority,
dueDate: dueDate !== undefined ? (dueDate ? new Date(dueDate) : null) : todo.dueDate,
@@ -199,6 +332,23 @@ export const updateTodo = async (todoId, data) => {
.where(eq(todos.id, todoId))
.returning();
// Update assigned users if provided
if (assignedUserIds !== undefined) {
// Delete existing assignments
await db.delete(todoUsers).where(eq(todoUsers.todoId, todoId));
// Create new assignments
if (Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
const todoUserInserts = assignedUserIds.map((userId) => ({
todoId: todoId,
userId: userId,
assignedBy: null, // We don't track who made the update
}));
await db.insert(todoUsers).values(todoUserInserts);
}
}
return updated;
};
@@ -214,7 +364,7 @@ export const deleteTodo = async (todoId) => {
};
/**
* Get todo with related data (notes, project, company, assigned user)
* Get todo with related data (notes, project, company, assigned users)
*/
export const getTodoWithRelations = async (todoId) => {
const todo = await getTodoById(todoId);
@@ -239,20 +389,18 @@ export const getTodoWithRelations = async (todoId) => {
.limit(1);
}
// Get assigned user if exists
let assignedUser = null;
if (todo.assignedTo) {
[assignedUser] = await db
.select({
id: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(users)
.where(eq(users.id, todo.assignedTo))
.limit(1);
}
// Get assigned users from todo_users junction table
const assignedUsers = await db
.select({
id: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
assignedAt: todoUsers.assignedAt,
})
.from(todoUsers)
.innerJoin(users, eq(todoUsers.userId, users.id))
.where(eq(todoUsers.todoId, todoId));
// Get related notes
const todoNotes = await db
@@ -265,7 +413,7 @@ export const getTodoWithRelations = async (todoId) => {
...todo,
project,
company,
assignedUser,
assignedUsers,
notes: todoNotes,
};
};
@@ -296,9 +444,20 @@ export const getTodosByCompanyId = async (companyId) => {
* Get todos assigned to a user
*/
export const getTodosByUserId = async (userId) => {
const todoIdsWithUser = await db
.select({ todoId: todoUsers.todoId })
.from(todoUsers)
.where(eq(todoUsers.userId, userId));
const todoIds = todoIdsWithUser.map((row) => row.todoId);
if (todoIds.length === 0) {
return [];
}
return await db
.select()
.from(todos)
.where(eq(todos.assignedTo, userId))
.where(inArray(todos.id, todoIds))
.orderBy(desc(todos.createdAt));
};