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:
Ulas Kalayci
2026-05-06 10:04:41 +02:00
parent f0503f3df1
commit 2a48fb7af0
12 changed files with 648 additions and 146 deletions
+11
View File
@@ -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 (