diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 88cb03b..529e542 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,52 @@ TASKMATE - CHANGELOG ==================== +================================================================================ +18.04.2026 - v403 - Bugfix: Archivierung + Task-Duplizieren +================================================================================ +PROBLEM +-------------------------------------------------------------------------------- +1. Archivierte Aufgaben konnten nicht wiederhergestellt werden: Backend-Update + lief durch, aber das Board zeigte den Task nicht zurueck. Grund: Archivierte + Tasks sind nie im Frontend-Store (API filtert per Default archived=0), und + store.updateTask() ist ein No-op fuer unbekannte IDs. Ausserdem hatte der + Socket-Event 'task:archived' keinen Frontend-Handler, d. h. andere Clients + und Ansichten (Kalender, Liste, Mobile) blieben out-of-sync. +2. Beim Duplizieren gingen abgehakte Subtasks verloren: INSERT kopierte nur + (task_id, title, position) - das 'completed'-Feld fiel auf Schema-Default 0. + Zusaetzlich wurden task_assignees (Mehrfachzuweisungen) nicht mitkopiert. + +LOESUNG +-------------------------------------------------------------------------------- +Backend (backend/routes/tasks.js): +- Duplicate-Route: INSERT INTO subtasks erweitert um Spalte 'completed' +- Duplicate-Route: task_assignees analog zu task_labels mitkopieren +- Archive-Route: Vollstaendigen Task im Socket-Event mitschicken (task + userId) +- Archive-Route: res.json() liefert jetzt auch den aktualisierten Task zurueck + +Frontend (frontend/js/sync.js): +- Neuer Socket-Handler 'task:archived': entfernt Task aus Board bei archiv=true, + fuegt ihn bei archiv=false via store.addTask/updateTask wieder ein +- Eigene UserID wird wie bei anderen Events ignoriert + +Frontend (frontend/js/app.js, task-modal.js): +- app.restoreTask + handleRestore + showColumnSelectDialog: nutzen den vom + Backend zurueckgegebenen Task, um ihn in den Store einzupflegen statt nur + store.updateTask() zu rufen (welches bei nicht vorhandenen Tasks wirkungslos ist) +- handleRestore schliesst jetzt zusaetzlich das archive-modal via + 'modal:close'-Event, damit der Nutzer den wiederhergestellten Task sofort + im Board sieht +- Neue Helper-Methode applyRestoredTask() im task-modal + +Service Worker: +- CACHE_VERSION: 402 -> 403 (erzwingt Neulade der Frontend-Assets) + +Geaenderte Dateien: +- backend/routes/tasks.js +- frontend/js/sync.js +- frontend/js/app.js +- frontend/js/task-modal.js +- frontend/sw.js + ================================================================================ 19.03.2026 - v402 - Assistent: Umbau auf Claude-Proxy (HTTP/SSE) diff --git a/backend/routes/tasks.js b/backend/routes/tasks.js index 6017fc1..baf5e50 100644 --- a/backend/routes/tasks.js +++ b/backend/routes/tasks.js @@ -799,10 +799,15 @@ router.post('/:id/duplicate', (req, res) => { const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)'); taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id)); - // Subtasks kopieren + // Mehrfachzuweisungen kopieren + const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId); + const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)'); + assignees.forEach(a => insertAssignee.run(newTaskId, a.user_id)); + + // Subtasks inkl. Abhak-Status kopieren const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId); - const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)'); - subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, idx)); + const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, completed, position) VALUES (?, ?, ?, ?)'); + subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, st.completed ? 1 : 0, idx)); addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`); @@ -865,16 +870,18 @@ router.put('/:id/archive', (req, res) => { logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`); - // WebSocket - vollständige Task-Daten senden + // WebSocket - vollständigen Task mitschicken, damit Clients ihn wieder einpflegen können const io = req.app.get('io'); const updatedTask = getFullTask(db, taskId); io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived, - columnId: updatedTask?.columnId + columnId: updatedTask?.columnId, + task: updatedTask, + userId: req.user.id }); - res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' }); + res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt', task: updatedTask }); } catch (error) { logger.error('Fehler beim Archivieren:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); diff --git a/frontend/js/app.js b/frontend/js/app.js index b2be5df..d611c05 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -924,8 +924,21 @@ class App { async restoreTask(taskId) { try { const projectId = store.get('currentProjectId'); - await api.restoreTask(projectId, taskId); - store.updateTask(taskId, { archived: false }); + const response = await api.restoreTask(projectId, taskId); + + // Task in den Store einfuegen (vorher nur in Archivliste, nicht im Store) + const restoredTask = response?.task; + if (restoredTask) { + const existing = store.getTaskById ? store.getTaskById(taskId) : null; + if (existing) { + store.updateTask(taskId, { ...restoredTask, archived: false }); + } else { + store.addTask({ ...restoredTask, archived: false }); + } + } else { + store.updateTask(taskId, { archived: false }); + } + this.showSuccess('Aufgabe wiederhergestellt'); // Re-render board diff --git a/frontend/js/sync.js b/frontend/js/sync.js index d45a7f7..4a03244 100644 --- a/frontend/js/sync.js +++ b/frontend/js/sync.js @@ -163,6 +163,10 @@ class SyncManager { this.handleTaskMoved(data); }); + this.socket.on('task:archived', (data) => { + this.handleTaskArchived(data); + }); + this.socket.on('label:created', (data) => { this.handleLabelCreated(data); }); @@ -369,6 +373,27 @@ class SyncManager { store.moveTask(taskId, columnId, position); } + handleTaskArchived(data) { + const { id, archived, task, userId } = data; + + if (userId && userId === store.get('currentUser')?.id) return; + + if (archived) { + // Archivieren: aus Board-Sicht entfernen (Task bleibt in DB) + store.removeTask(id); + } else { + // Wiederherstellen: Task zurück in den Store bringen + if (task) { + const existing = store.getTaskById ? store.getTaskById(id) : null; + if (existing) { + store.updateTask(id, { ...task, archived: false }); + } else { + store.addTask({ ...task, archived: false }); + } + } + } + } + handleLabelCreated(data) { const { label, userId } = data; diff --git a/frontend/js/task-modal.js b/frontend/js/task-modal.js index 5593d7b..fc16a9c 100644 --- a/frontend/js/task-modal.js +++ b/frontend/js/task-modal.js @@ -588,9 +588,10 @@ class TaskModalManager { async handleRestore() { try { const projectId = store.get('currentProjectId'); - await api.restoreTask(projectId, this.taskId); - store.updateTask(this.taskId, { archived: false }); + const response = await api.restoreTask(projectId, this.taskId); + this.applyRestoredTask(response?.task); this.close(); + window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'archive-modal' } })); this.showSuccess('Aufgabe wiederhergestellt'); } catch (error) { // Prüfen ob Spaltenauswahl erforderlich ist @@ -602,6 +603,19 @@ class TaskModalManager { } } + applyRestoredTask(restoredTask) { + if (restoredTask) { + const existing = store.getTaskById ? store.getTaskById(this.taskId) : null; + if (existing) { + store.updateTask(this.taskId, { ...restoredTask, archived: false }); + } else { + store.addTask({ ...restoredTask, archived: false }); + } + } else { + store.updateTask(this.taskId, { archived: false }); + } + } + handleTaskToAssistant() { if (!this.originalTask) return; @@ -640,9 +654,10 @@ class TaskModalManager { onSelect: async (columnId) => { try { const projectId = store.get('currentProjectId'); - await api.restoreTask(projectId, this.taskId, columnId); - store.updateTask(this.taskId, { archived: false, columnId: columnId }); + const response = await api.restoreTask(projectId, this.taskId, columnId); + this.applyRestoredTask(response?.task); this.close(); + window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'archive-modal' } })); this.showSuccess('Aufgabe wiederhergestellt'); } catch (error) { this.showError('Fehler beim Wiederherstellen'); diff --git a/frontend/sw.js b/frontend/sw.js index 8f22ace..b439867 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -4,7 +4,7 @@ * Offline support and caching */ -const CACHE_VERSION = '402'; +const CACHE_VERSION = '403'; const CACHE_NAME = 'taskmate-v' + CACHE_VERSION; const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION; const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;