feat: multi-person assignment for tasks and calendar events
- DB migration v32: task_assignments and event_assignments join tables with CASCADE delete; existing assigned_to data migrated automatically - Tasks API: accepts assigned_to as array, returns assigned_users[] with json_group_array; filter uses EXISTS on task_assignments - Calendar API: same pattern via event_assignments; serializeEvent includes assigned_users array - Recurring task completion copies all assignments to the new instance - Frontend: shared UserMultiSelect component with avatar stack display (renderAvatarStack, renderUserMultiSelect, getSelectedUserIds, bindUserMultiSelect); tasks.js and calendar.js use it in modals and card/agenda views - CSS: user-multi-select.css with avatar-stack and user-ms classes - 14 new tests covering CRUD, JSON aggregation, EXISTS filter, and CASCADE behavior for both task and event assignments Closes #125
This commit is contained in:
@@ -252,6 +252,17 @@ const MIGRATIONS_SQL = {
|
||||
CREATE INDEX IF NOT EXISTS idx_birthdays_calendar_ref ON birthdays(calendar_event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_created_by ON api_tokens(created_by);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_assignments (
|
||||
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (task_id, user_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS event_assignments (
|
||||
event_id INTEGER NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (event_id, user_id)
|
||||
);
|
||||
`,
|
||||
2: `
|
||||
CREATE TABLE IF NOT EXISTS sync_config (
|
||||
|
||||
@@ -1189,6 +1189,26 @@ const MIGRATIONS = [
|
||||
ALTER TABLE birthdays ADD COLUMN reminder_custom_unit TEXT;
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 32,
|
||||
description: 'Multi-person assignment for tasks and calendar events',
|
||||
up: `
|
||||
CREATE TABLE IF NOT EXISTS task_assignments (
|
||||
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (task_id, user_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS event_assignments (
|
||||
event_id INTEGER NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (event_id, user_id)
|
||||
);
|
||||
INSERT OR IGNORE INTO task_assignments (task_id, user_id)
|
||||
SELECT id, assigned_to FROM tasks WHERE assigned_to IS NOT NULL;
|
||||
INSERT OR IGNORE INTO event_assignments (event_id, user_id)
|
||||
SELECT id, assigned_to FROM calendar_events WHERE assigned_to IS NOT NULL;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+106
-68
@@ -94,10 +94,33 @@ function attachmentDataUrl(event) {
|
||||
return `data:${event.attachment_mime};base64,${event.attachment_data}`;
|
||||
}
|
||||
|
||||
const ASSIGNED_USERS_SQL = `(
|
||||
SELECT json_group_array(json_object(
|
||||
'id', u.id, 'display_name', u.display_name, 'color', u.avatar_color
|
||||
))
|
||||
FROM event_assignments ea JOIN users u ON u.id = ea.user_id
|
||||
WHERE ea.event_id = e.id
|
||||
) AS assigned_users_json`;
|
||||
|
||||
function parseAssignedTo(val) {
|
||||
if (Array.isArray(val)) return val.map(Number).filter(Boolean);
|
||||
if (val !== null && val !== undefined && val !== '') return [Number(val)].filter(Boolean);
|
||||
return [];
|
||||
}
|
||||
|
||||
function setEventAssignments(d, eventId, userIds) {
|
||||
d.prepare('DELETE FROM event_assignments WHERE event_id = ?').run(eventId);
|
||||
const ins = d.prepare('INSERT OR IGNORE INTO event_assignments (event_id, user_id) VALUES (?, ?)');
|
||||
for (const uid of userIds) ins.run(eventId, uid);
|
||||
}
|
||||
|
||||
function serializeEvent(event) {
|
||||
if (!event) return event;
|
||||
const assigned_users = event.assigned_users_json ? JSON.parse(event.assigned_users_json) : [];
|
||||
const { assigned_users_json, ...rest } = event;
|
||||
return {
|
||||
...event,
|
||||
...rest,
|
||||
assigned_users,
|
||||
attachment_data: attachmentDataUrl(event),
|
||||
};
|
||||
}
|
||||
@@ -207,7 +230,8 @@ router.get('/', (req, res) => {
|
||||
u_assigned.avatar_color AS assigned_color,
|
||||
u_created.display_name AS creator_name,
|
||||
ec.name AS cal_name,
|
||||
ec.color AS cal_color
|
||||
ec.color AS cal_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM calendar_events e
|
||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
||||
LEFT JOIN users u_created ON u_created.id = e.created_by
|
||||
@@ -229,7 +253,7 @@ router.get('/', (req, res) => {
|
||||
const params = [to, from, to, getUserId(req)];
|
||||
|
||||
if (req.query.assigned_to) {
|
||||
sql += ' AND e.assigned_to = ?';
|
||||
sql += ' AND EXISTS (SELECT 1 FROM event_assignments ea WHERE ea.event_id = e.id AND ea.user_id = ?)';
|
||||
params.push(parseInt(req.query.assigned_to, 10));
|
||||
}
|
||||
|
||||
@@ -267,7 +291,8 @@ router.get('/upcoming', (req, res) => {
|
||||
u_assigned.display_name AS assigned_name,
|
||||
u_assigned.avatar_color AS assigned_color,
|
||||
ec.name AS cal_name,
|
||||
ec.color AS cal_color
|
||||
ec.color AS cal_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM calendar_events e
|
||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
||||
LEFT JOIN external_calendars ec ON ec.id = e.calendar_ref_id
|
||||
@@ -580,7 +605,8 @@ router.get('/:id', (req, res) => {
|
||||
SELECT e.*,
|
||||
u_assigned.display_name AS assigned_name,
|
||||
u_assigned.avatar_color AS assigned_color,
|
||||
u_created.display_name AS creator_name
|
||||
u_created.display_name AS creator_name,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM calendar_events e
|
||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
||||
LEFT JOIN users u_created ON u_created.id = e.created_by
|
||||
@@ -628,43 +654,45 @@ router.post('/', (req, res) => {
|
||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||
if (!vIcon) return res.status(400).json({ error: 'icon: invalid calendar event icon.', code: 400 });
|
||||
|
||||
const { all_day = 0, assigned_to = null } = req.body;
|
||||
|
||||
if (assigned_to) {
|
||||
const user = db.get().prepare('SELECT id FROM users WHERE id = ?').get(assigned_to);
|
||||
if (!user) return res.status(400).json({ error: 'assigned_to: Benutzer nicht gefunden', code: 400 });
|
||||
}
|
||||
const { all_day = 0 } = req.body;
|
||||
const userIds = parseAssignedTo(req.body.assigned_to);
|
||||
const firstUid = userIds[0] ?? null;
|
||||
|
||||
const attachment = req.body.attachment_data ? parseAttachment(req.body.attachment_data) : { mime: null, size: null, data: null };
|
||||
|
||||
const result = db.get().prepare(`
|
||||
INSERT INTO calendar_events
|
||||
(title, description, start_datetime, end_datetime, all_day,
|
||||
location, color, icon, assigned_to, created_by, recurrence_rule,
|
||||
attachment_name, attachment_mime, attachment_size, attachment_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
vTitle.value, vDesc.value,
|
||||
vStart.value, vEnd.value,
|
||||
all_day ? 1 : 0, vLoc.value,
|
||||
vColor.value, vIcon, assigned_to || null,
|
||||
userId, vRrule.value,
|
||||
req.body.attachment_name || null,
|
||||
attachment.mime,
|
||||
attachment.size,
|
||||
attachment.data
|
||||
);
|
||||
const eventId = db.get().transaction(() => {
|
||||
const result = db.get().prepare(`
|
||||
INSERT INTO calendar_events
|
||||
(title, description, start_datetime, end_datetime, all_day,
|
||||
location, color, icon, assigned_to, created_by, recurrence_rule,
|
||||
attachment_name, attachment_mime, attachment_size, attachment_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
vTitle.value, vDesc.value,
|
||||
vStart.value, vEnd.value,
|
||||
all_day ? 1 : 0, vLoc.value,
|
||||
vColor.value, vIcon, firstUid,
|
||||
userId, vRrule.value,
|
||||
req.body.attachment_name || null,
|
||||
attachment.mime,
|
||||
attachment.size,
|
||||
attachment.data
|
||||
);
|
||||
setEventAssignments(db.get(), result.lastInsertRowid, userIds);
|
||||
return result.lastInsertRowid;
|
||||
})();
|
||||
|
||||
const event = db.get().prepare(`
|
||||
SELECT e.*,
|
||||
u_assigned.display_name AS assigned_name,
|
||||
u_assigned.avatar_color AS assigned_color,
|
||||
u_created.display_name AS creator_name
|
||||
u_created.display_name AS creator_name,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM calendar_events e
|
||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
||||
LEFT JOIN users u_created ON u_created.id = e.created_by
|
||||
WHERE e.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
`).get(eventId);
|
||||
|
||||
res.status(201).json({ data: serializeEvent(event) });
|
||||
} catch (err) {
|
||||
@@ -707,53 +735,63 @@ router.put('/:id', (req, res) => {
|
||||
|
||||
const {
|
||||
title, description, start_datetime, end_datetime,
|
||||
all_day, location, color: colorVal, assigned_to, recurrence_rule, attachment_name,
|
||||
all_day, location, color: colorVal, recurrence_rule, attachment_name,
|
||||
} = req.body;
|
||||
|
||||
const userIds = req.body.assigned_to !== undefined
|
||||
? parseAssignedTo(req.body.assigned_to)
|
||||
: db.get().prepare('SELECT user_id FROM event_assignments WHERE event_id = ?')
|
||||
.all(id).map((r) => r.user_id);
|
||||
const firstUid = userIds[0] ?? null;
|
||||
|
||||
const userModified = event.external_source !== 'local' ? 1 : event.user_modified;
|
||||
|
||||
db.get().prepare(`
|
||||
UPDATE calendar_events
|
||||
SET title = COALESCE(?, title),
|
||||
description = ?,
|
||||
start_datetime = COALESCE(?, start_datetime),
|
||||
end_datetime = ?,
|
||||
all_day = COALESCE(?, all_day),
|
||||
location = ?,
|
||||
color = COALESCE(?, color),
|
||||
icon = COALESCE(?, icon),
|
||||
assigned_to = ?,
|
||||
recurrence_rule = ?,
|
||||
attachment_name = ?,
|
||||
attachment_mime = ?,
|
||||
attachment_size = ?,
|
||||
attachment_data = ?,
|
||||
user_modified = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
title?.trim() ?? null,
|
||||
description !== undefined ? (description || null) : event.description,
|
||||
start_datetime ?? null,
|
||||
end_datetime !== undefined ? (end_datetime || null) : event.end_datetime,
|
||||
all_day !== undefined ? (all_day ? 1 : 0) : null,
|
||||
location !== undefined ? (location || null) : event.location,
|
||||
colorVal ?? null,
|
||||
req.body.icon !== undefined ? vIcon : null,
|
||||
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
|
||||
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
|
||||
attachment_name !== undefined ? (attachment_name || null) : event.attachment_name,
|
||||
attachment.mime,
|
||||
attachment.size,
|
||||
attachment.data,
|
||||
userModified,
|
||||
id
|
||||
);
|
||||
db.get().transaction(() => {
|
||||
db.get().prepare(`
|
||||
UPDATE calendar_events
|
||||
SET title = COALESCE(?, title),
|
||||
description = ?,
|
||||
start_datetime = COALESCE(?, start_datetime),
|
||||
end_datetime = ?,
|
||||
all_day = COALESCE(?, all_day),
|
||||
location = ?,
|
||||
color = COALESCE(?, color),
|
||||
icon = COALESCE(?, icon),
|
||||
assigned_to = ?,
|
||||
recurrence_rule = ?,
|
||||
attachment_name = ?,
|
||||
attachment_mime = ?,
|
||||
attachment_size = ?,
|
||||
attachment_data = ?,
|
||||
user_modified = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
title?.trim() ?? null,
|
||||
description !== undefined ? (description || null) : event.description,
|
||||
start_datetime ?? null,
|
||||
end_datetime !== undefined ? (end_datetime || null) : event.end_datetime,
|
||||
all_day !== undefined ? (all_day ? 1 : 0) : null,
|
||||
location !== undefined ? (location || null) : event.location,
|
||||
colorVal ?? null,
|
||||
req.body.icon !== undefined ? vIcon : null,
|
||||
firstUid !== undefined ? firstUid : event.assigned_to,
|
||||
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
|
||||
attachment_name !== undefined ? (attachment_name || null) : event.attachment_name,
|
||||
attachment.mime,
|
||||
attachment.size,
|
||||
attachment.data,
|
||||
userModified,
|
||||
id
|
||||
);
|
||||
setEventAssignments(db.get(), id, userIds);
|
||||
})();
|
||||
|
||||
const updated = db.get().prepare(`
|
||||
SELECT e.*,
|
||||
u_assigned.display_name AS assigned_name,
|
||||
u_assigned.avatar_color AS assigned_color,
|
||||
u_created.display_name AS creator_name
|
||||
u_created.display_name AS creator_name,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM calendar_events e
|
||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
||||
LEFT JOIN users u_created ON u_created.id = e.created_by
|
||||
|
||||
+95
-39
@@ -27,15 +27,42 @@ const VALID_CATEGORIES = ['household', 'school', 'shopping', 'repair',
|
||||
// Hilfsfunktionen
|
||||
// --------------------------------------------------------
|
||||
|
||||
const ASSIGNED_USERS_SQL = `(
|
||||
SELECT json_group_array(json_object(
|
||||
'id', u.id, 'display_name', u.display_name, 'color', u.avatar_color
|
||||
))
|
||||
FROM task_assignments ta JOIN users u ON u.id = ta.user_id
|
||||
WHERE ta.task_id = t.id
|
||||
) AS assigned_users_json`;
|
||||
|
||||
function addAssignedUsers(task) {
|
||||
task.assigned_users = task.assigned_users_json ? JSON.parse(task.assigned_users_json) : [];
|
||||
delete task.assigned_users_json;
|
||||
return task;
|
||||
}
|
||||
|
||||
function parseAssignedTo(val) {
|
||||
if (Array.isArray(val)) return val.map(Number).filter(Boolean);
|
||||
if (val !== null && val !== undefined && val !== '') return [Number(val)].filter(Boolean);
|
||||
return [];
|
||||
}
|
||||
|
||||
function setAssignments(d, taskId, userIds) {
|
||||
d.prepare('DELETE FROM task_assignments WHERE task_id = ?').run(taskId);
|
||||
const ins = d.prepare('INSERT OR IGNORE INTO task_assignments (task_id, user_id) VALUES (?, ?)');
|
||||
for (const uid of userIds) ins.run(taskId, uid);
|
||||
}
|
||||
|
||||
/** Alle Subtasks einer Aufgabe laden (eine Ebene tief). */
|
||||
function loadSubtasks(taskId) {
|
||||
return db.get().prepare(`
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM tasks t
|
||||
LEFT JOIN users u ON t.assigned_to = u.id
|
||||
WHERE t.parent_task_id = ?
|
||||
ORDER BY t.created_at ASC
|
||||
`).all(taskId);
|
||||
`).all(taskId).map(addAssignedUsers);
|
||||
}
|
||||
|
||||
/** Fortschritt der Subtasks berechnen (erledigte / gesamt). */
|
||||
@@ -79,6 +106,7 @@ router.get('/', (req, res) => {
|
||||
t.*,
|
||||
u.display_name AS assigned_name,
|
||||
u.avatar_color AS assigned_color,
|
||||
${ASSIGNED_USERS_SQL},
|
||||
(SELECT COUNT(*) FROM tasks s WHERE s.parent_task_id = t.id) AS subtask_total,
|
||||
(SELECT COUNT(*) FROM tasks s WHERE s.parent_task_id = t.id AND s.status = 'done') AS subtask_done
|
||||
FROM tasks t
|
||||
@@ -89,7 +117,10 @@ router.get('/', (req, res) => {
|
||||
|
||||
if (status) { sql += ' AND t.status = ?'; params.push(status); }
|
||||
if (priority) { sql += ' AND t.priority = ?'; params.push(priority); }
|
||||
if (assigned_to) { sql += ' AND t.assigned_to = ?'; params.push(Number(assigned_to)); }
|
||||
if (assigned_to) {
|
||||
sql += ' AND EXISTS (SELECT 1 FROM task_assignments ta WHERE ta.task_id = t.id AND ta.user_id = ?)';
|
||||
params.push(Number(assigned_to));
|
||||
}
|
||||
if (category) { sql += ' AND t.category = ?'; params.push(category); }
|
||||
|
||||
sql += `
|
||||
@@ -101,7 +132,7 @@ router.get('/', (req, res) => {
|
||||
t.created_at DESC
|
||||
`;
|
||||
|
||||
res.json({ data: db.get().prepare(sql).all(...params) });
|
||||
res.json({ data: db.get().prepare(sql).all(...params).map(addAssignedUsers) });
|
||||
} catch (err) {
|
||||
log.error('GET / error:', err);
|
||||
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||
@@ -116,7 +147,8 @@ router.get('/', (req, res) => {
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const task = db.get().prepare(`
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM tasks t
|
||||
LEFT JOIN users u ON t.assigned_to = u.id
|
||||
WHERE t.id = ? AND t.parent_task_id IS NULL
|
||||
@@ -124,6 +156,7 @@ router.get('/:id', (req, res) => {
|
||||
|
||||
if (!task) return res.status(404).json({ error: 'Task not found.', code: 404 });
|
||||
|
||||
addAssignedUsers(task);
|
||||
task.subtasks = loadSubtasks(task.id);
|
||||
res.json({ data: task });
|
||||
} catch (err) {
|
||||
@@ -151,12 +184,14 @@ router.post('/', (req, res) => {
|
||||
priority = 'none',
|
||||
due_date = null,
|
||||
due_time = null,
|
||||
assigned_to = null,
|
||||
parent_task_id = null,
|
||||
is_recurring = 0,
|
||||
recurrence_rule = null,
|
||||
} = req.body;
|
||||
|
||||
const userIds = parseAssignedTo(req.body.assigned_to);
|
||||
const firstUid = userIds[0] ?? null;
|
||||
|
||||
// Tiefe begrenzen: Subtasks dürfen keine eigenen Subtasks haben (max. 2 Ebenen)
|
||||
if (parent_task_id) {
|
||||
const parent = db.get().prepare('SELECT parent_task_id FROM tasks WHERE id = ?')
|
||||
@@ -166,24 +201,29 @@ router.post('/', (req, res) => {
|
||||
return res.status(400).json({ error: 'Maximal 2 Verschachtelungsebenen erlaubt.', code: 400 });
|
||||
}
|
||||
|
||||
const result = db.get().prepare(`
|
||||
INSERT INTO tasks
|
||||
(title, description, category, priority, due_date, due_time,
|
||||
assigned_to, created_by, parent_task_id, is_recurring, recurrence_rule)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
title.trim(), description, category, priority,
|
||||
due_date, due_time, assigned_to, req.session.userId, parent_task_id,
|
||||
is_recurring ? 1 : 0, recurrence_rule
|
||||
);
|
||||
const taskId = db.get().transaction(() => {
|
||||
const result = db.get().prepare(`
|
||||
INSERT INTO tasks
|
||||
(title, description, category, priority, due_date, due_time,
|
||||
assigned_to, created_by, parent_task_id, is_recurring, recurrence_rule)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
title.trim(), description, category, priority,
|
||||
due_date, due_time, firstUid, req.session.userId, parent_task_id,
|
||||
is_recurring ? 1 : 0, recurrence_rule
|
||||
);
|
||||
setAssignments(db.get(), result.lastInsertRowid, userIds);
|
||||
return result.lastInsertRowid;
|
||||
})();
|
||||
|
||||
const task = db.get().prepare(`
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id
|
||||
WHERE t.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
`).get(taskId);
|
||||
|
||||
res.status(201).json({ data: task });
|
||||
res.status(201).json({ data: addAssignedUsers(task) });
|
||||
} catch (err) {
|
||||
log.error('POST / error:', err);
|
||||
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||
@@ -213,26 +253,36 @@ router.put('/:id', (req, res) => {
|
||||
status = task.status,
|
||||
due_date = task.due_date,
|
||||
due_time = task.due_time,
|
||||
assigned_to = task.assigned_to,
|
||||
is_recurring = task.is_recurring,
|
||||
recurrence_rule = task.recurrence_rule,
|
||||
} = req.body;
|
||||
|
||||
db.get().prepare(`
|
||||
UPDATE tasks SET
|
||||
title = ?, description = ?, category = ?, priority = ?,
|
||||
status = ?, due_date = ?, due_time = ?, assigned_to = ?,
|
||||
is_recurring = ?, recurrence_rule = ?
|
||||
WHERE id = ?
|
||||
`).run(title.trim(), description, category, priority,
|
||||
status, due_date, due_time, assigned_to,
|
||||
is_recurring ? 1 : 0, recurrence_rule, req.params.id);
|
||||
const userIds = req.body.assigned_to !== undefined
|
||||
? parseAssignedTo(req.body.assigned_to)
|
||||
: db.get().prepare('SELECT user_id FROM task_assignments WHERE task_id = ?')
|
||||
.all(task.id).map((r) => r.user_id);
|
||||
const firstUid = userIds[0] ?? null;
|
||||
|
||||
db.get().transaction(() => {
|
||||
db.get().prepare(`
|
||||
UPDATE tasks SET
|
||||
title = ?, description = ?, category = ?, priority = ?,
|
||||
status = ?, due_date = ?, due_time = ?, assigned_to = ?,
|
||||
is_recurring = ?, recurrence_rule = ?
|
||||
WHERE id = ?
|
||||
`).run(title.trim(), description, category, priority,
|
||||
status, due_date, due_time, firstUid,
|
||||
is_recurring ? 1 : 0, recurrence_rule, req.params.id);
|
||||
setAssignments(db.get(), task.id, userIds);
|
||||
})();
|
||||
|
||||
const updated = db.get().prepare(`
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color
|
||||
SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color,
|
||||
${ASSIGNED_USERS_SQL}
|
||||
FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id
|
||||
WHERE t.id = ?
|
||||
`).get(req.params.id);
|
||||
addAssignedUsers(updated);
|
||||
updated.subtasks = loadSubtasks(updated.id);
|
||||
|
||||
res.json({ data: updated });
|
||||
@@ -266,15 +316,21 @@ router.patch('/:id/status', (req, res) => {
|
||||
if (task?.is_recurring && task.recurrence_rule && !task.parent_task_id) {
|
||||
const nextDate = nextOccurrence(task.due_date, task.recurrence_rule);
|
||||
if (nextDate) {
|
||||
db.get().prepare(`
|
||||
INSERT INTO tasks (title, description, category, priority, status,
|
||||
due_date, due_time, assigned_to, created_by, is_recurring, recurrence_rule)
|
||||
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, 1, ?)
|
||||
`).run(
|
||||
task.title, task.description, task.category, task.priority,
|
||||
nextDate, task.due_time, task.assigned_to, task.created_by,
|
||||
task.recurrence_rule
|
||||
);
|
||||
const existingAssignments = db.get()
|
||||
.prepare('SELECT user_id FROM task_assignments WHERE task_id = ?')
|
||||
.all(task.id).map((r) => r.user_id);
|
||||
db.get().transaction(() => {
|
||||
const newTask = db.get().prepare(`
|
||||
INSERT INTO tasks (title, description, category, priority, status,
|
||||
due_date, due_time, assigned_to, created_by, is_recurring, recurrence_rule)
|
||||
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, 1, ?)
|
||||
`).run(
|
||||
task.title, task.description, task.category, task.priority,
|
||||
nextDate, task.due_time, task.assigned_to, task.created_by,
|
||||
task.recurrence_rule
|
||||
);
|
||||
setAssignments(db.get(), newTask.lastInsertRowid, existingAssignments);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user