From 513de7813fb9df7fb28574d7cc6513bf507dccb0 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2026 20:32:15 +0700 Subject: [PATCH] feat: replace subtasks with task commenting system and update task view UI --- index.php | 26 +++++----- views/tasks.php | 131 +++++++++++++++++++++++++----------------------- 2 files changed, 82 insertions(+), 75 deletions(-) diff --git a/index.php b/index.php index 9985aaf..e9967df 100644 --- a/index.php +++ b/index.php @@ -18,7 +18,7 @@ if ($action === 'api') { try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completed_by INTEGER DEFAULT NULL"); } catch(Exception $e) {} try { $pdo->exec("ALTER TABLE tasks ADD COLUMN archived_by INTEGER DEFAULT NULL"); } catch(Exception $e) {} try { $pdo->exec("CREATE TABLE IF NOT EXISTS task_attachments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, file_name TEXT, file_path TEXT, owner_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {} - try { $pdo->exec("CREATE TABLE IF NOT EXISTS task_subtasks (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, title TEXT NOT NULL, is_completed INTEGER DEFAULT 0, created_by INTEGER, completed_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {} + try { $pdo->exec("CREATE TABLE IF NOT EXISTS task_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, comment TEXT NOT NULL, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {} $format_dt = function($value) { $value = is_string($value) ? trim($value) : $value; @@ -62,27 +62,27 @@ if ($action === 'api') { exit; } - if($api_action === 'add_subtask') { + if($api_action === 'add_comment') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); $task_id = $_POST['task_id'] ?? null; - $title = $_POST['title'] ?? null; + $comment = $_POST['comment'] ?? null; $user_id = $_SESSION['user_id']; - if($task_id && $title) { - $stmt = $pdo->prepare("INSERT INTO task_subtasks (task_id, title, created_by) VALUES (?, ?, ?)"); - $stmt->execute([$task_id, $title, $user_id]); + if($task_id && $comment) { + $stmt = $pdo->prepare("INSERT INTO task_comments (task_id, comment, created_by) VALUES (?, ?, ?)"); + $stmt->execute([$task_id, $comment, $user_id]); + $t = query("SELECT title FROM tasks WHERE id=?", [$task_id])->fetch(); + if($t) notify_waha('Commented on', 'Task', $t['title']); } echo json_encode(['status'=>'ok']); exit; } - if($api_action === 'toggle_subtask') { + if($api_action === 'delete_comment') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); - $subtask_id = $_POST['subtask_id'] ?? null; - $is_completed = (int)($_POST['is_completed'] ?? 0); - $user_id = $_SESSION['user_id']; - if($subtask_id) { - $stmt = $pdo->prepare("UPDATE task_subtasks SET is_completed = ?, completed_by = ? WHERE id = ?"); - $stmt->execute([$is_completed, $is_completed ? $user_id : null, $subtask_id]); + $comment_id = $_POST['comment_id'] ?? null; + if($comment_id) { + $stmt = $pdo->prepare("DELETE FROM task_comments WHERE id = ?"); + $stmt->execute([$comment_id]); } echo json_encode(['status'=>'ok']); exit; diff --git a/views/tasks.php b/views/tasks.php index aa598b1..234f166 100644 --- a/views/tasks.php +++ b/views/tasks.php @@ -221,7 +221,7 @@ $tasks = $stmt->fetchAll(); $t_ids = array_column($tasks, 'id'); $attachments = []; -$subtasks = []; +$comments = []; if($t_ids) { $in = str_repeat('?,', count($t_ids) - 1) . '?'; $stmt = $pdo->prepare("SELECT ta.*, u.username as owner_name FROM task_attachments ta LEFT JOIN users u ON ta.owner_id = u.id WHERE ta.task_id IN ($in) ORDER BY ta.created_at ASC"); @@ -230,10 +230,10 @@ if($t_ids) { $attachments[$row['task_id']][] = $row; } - $stmt2 = $pdo->prepare("SELECT ts.*, cbu.username as completed_by_username FROM task_subtasks ts LEFT JOIN users cbu ON ts.completed_by = cbu.id WHERE ts.task_id IN ($in) ORDER BY ts.created_at ASC"); + $stmt2 = $pdo->prepare("SELECT tc.*, cu.username as created_by_username FROM task_comments tc LEFT JOIN users cu ON tc.created_by = cu.id WHERE tc.task_id IN ($in) ORDER BY tc.created_at ASC"); $stmt2->execute($t_ids); foreach($stmt2->fetchAll() as $row) { - $subtasks[$row['task_id']][] = $row; + $comments[$row['task_id']][] = $row; } } @@ -560,12 +560,10 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l echo ' '.$att_count.''; } - // We could show subtasks count too on collapsed state - $sub_count = empty($subtasks[$t['id']]) ? 0 : count($subtasks[$t['id']]); - $sub_done = 0; - if($sub_count > 0) { - foreach($subtasks[$t['id']] as $st) if($st['is_completed']) $sub_done++; - echo ' '.$sub_done.'/'.$sub_count.''; + // We could show comments count too on collapsed state + $comment_count = empty($comments[$t['id']]) ? 0 : count($comments[$t['id']]); + if($comment_count > 0) { + echo ' '.$comment_count.''; } if($t['due_date']) { @@ -588,32 +586,44 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
-
-
📅 Created:
+
+
Created:
-
⏰ Due Date:
+
Due Date:
+ '.$initial.'
'; + } + } + ?>
- From: - - - -
-
- To: - - - + Assignments: +
+
+ From + +
+
|
+
+ To + +
+
@@ -633,38 +643,35 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
-
-
Subtasks / Progress
-
- - -
- onchange="toggleSubtask(, this.checked)"> -
- - - - -
- Completed by -
- +
Comments
+
+ + +
+ +
+
+ + + +   + + +
+
-
No progress updates yet.
+
No comments yet.
- -
- - -
- +
+ + +
-
@@ -859,25 +866,25 @@ function drop(ev, newStatus) { setTimeout(() => window.location.reload(), 300); }); } -function addSubtask(taskId, form) { - let title = form.title.value; - if(!title) return; - fetch('?action=api&api_action=add_subtask', { +function addComment(taskId, form) { + let comment = form.comment.value; + if(!comment) return; + fetch('?action=api&api_action=add_comment', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: `task_id=${taskId}&title=${encodeURIComponent(title)}` + body: `task_id=${taskId}&comment=${encodeURIComponent(comment)}` }).then(() => { form.reset(); window.location.reload(); }); } -function toggleSubtask(subtaskId, isCompleted) { - fetch('?action=api&api_action=toggle_subtask', { +function deleteComment(commentId) { + if(!confirm('Delete this comment?')) return; + fetch('?action=api&api_action=delete_comment', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: `subtask_id=${subtaskId}&is_completed=${isCompleted ? 1 : 0}` + body: `comment_id=${commentId}` }).then(() => { - // Quick visual toggle doesn't strictly need reload, but reloading ensures names are updated window.location.reload(); }); }