diff --git a/index.php b/index.php
index 39303ef..9985aaf 100644
--- a/index.php
+++ b/index.php
@@ -15,7 +15,10 @@ if ($action === 'api') {
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN status_updated_at DATETIME"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN due_date DATE"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN custom_assignee_name TEXT"); } catch(Exception $e) {}
+ 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) {}
$format_dt = function($value) {
$value = is_string($value) ? trim($value) : $value;
@@ -37,14 +40,54 @@ if ($action === 'api') {
if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized']));
$task_id = $_POST['task_id'] ?? null;
$status = $_POST['status'] ?? null;
- $stmt = $pdo->prepare("UPDATE tasks SET status = ?, status_updated_at = CURRENT_TIMESTAMP WHERE id = ?");
- $stmt->execute([$status, $task_id]);
+ $user_id = $_SESSION['user_id'];
+
+ $set_clause = "status = ?, status_updated_at = CURRENT_TIMESTAMP";
+ $params = [$status];
+
+ if ($status === 'completed') {
+ $set_clause .= ", completed_by = ?";
+ $params[] = $user_id;
+ } elseif ($status === 'archived') {
+ $set_clause .= ", archived_by = ?";
+ $params[] = $user_id;
+ }
+ $params[] = $task_id;
+
+ $stmt = $pdo->prepare("UPDATE tasks SET $set_clause WHERE id = ?");
+ $stmt->execute($params);
$task_to_upd = query("SELECT title FROM tasks WHERE id=?", [$task_id])->fetch();
if($task_to_upd) notify_waha('Moved to '.$status, 'Task Status', $task_to_upd['title']);
echo json_encode(['status'=>'ok']);
exit;
}
+ if($api_action === 'add_subtask') {
+ if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized']));
+ $task_id = $_POST['task_id'] ?? null;
+ $title = $_POST['title'] ?? 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]);
+ }
+ echo json_encode(['status'=>'ok']);
+ exit;
+ }
+
+ if($api_action === 'toggle_subtask') {
+ 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]);
+ }
+ echo json_encode(['status'=>'ok']);
+ exit;
+ }
+
if($api_action === 'get_tasks_status') {
$task_id = $_GET['task_id'] ?? null;
diff --git a/views/admin.php b/views/admin.php
index 7379180..af0495a 100644
--- a/views/admin.php
+++ b/views/admin.php
@@ -459,7 +459,7 @@ $active_tab = $_GET['tab'] ?? 'overview';
diff --git a/views/tasks.php b/views/tasks.php
index 4b53d21..aa598b1 100644
--- a/views/tasks.php
+++ b/views/tasks.php
@@ -194,12 +194,14 @@ $custom_like = $custom_tokens ? implode(' OR ', array_fill(0, count($custom_toke
$params = array_merge([$user_id], $my_group_ids, $my_group_ids, [$user_id], $custom_tokens, $custom_tokens);
-$sql = "SELECT t.*, u.username as assignor_user_name, g.name as assignor_group_name, au.username as assignee_user_name, ag.name as assignee_group_name
+$sql = "SELECT t.*, u.username as assignor_user_name, g.name as assignor_group_name, au.username as assignee_user_name, ag.name as assignee_group_name, cu.username as completed_by_username, aru.username as archived_by_username
FROM tasks t
LEFT JOIN users u ON t.assignor_id = u.id
LEFT JOIN groups g ON t.assignor_group_id = g.id
LEFT JOIN users au ON (t.assignee_type='user' AND t.assignee_id = au.id)
LEFT JOIN groups ag ON (t.assignee_type='group' AND t.assignee_id = ag.id)
+ LEFT JOIN users cu ON t.completed_by = cu.id
+ LEFT JOIN users aru ON t.archived_by = aru.id
WHERE (t.assignee_type='user' AND t.assignee_id = ?)
OR (t.assignee_type='group' AND t.assignee_id IN ($group_in))
OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in))
@@ -219,6 +221,7 @@ $tasks = $stmt->fetchAll();
$t_ids = array_column($tasks, 'id');
$attachments = [];
+$subtasks = [];
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");
@@ -226,6 +229,12 @@ if($t_ids) {
foreach($stmt->fetchAll() as $row) {
$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->execute($t_ids);
+ foreach($stmt2->fetchAll() as $row) {
+ $subtasks[$row['task_id']][] = $row;
+ }
}
$kanban = ['pending' => [], 'in_progress' => [], 'completed' => [], 'archived' => []];
@@ -266,7 +275,7 @@ usort($kanban['pending'], function($a, $b) {
.kanban-task {
background: var(--surface); padding: 1rem; border-radius: 10px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.03);
- border: 1px solid rgba(0,0,0,0.05); cursor: grab; transition: transform 0.2s, box-shadow 0.2s;
+ border: 1px solid rgba(0,0,0,0.05); cursor: grab; transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
border-left: 4px solid var(--border);
}
body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-left: 4px solid var(--primary); }
@@ -277,6 +286,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
.kanban-task.expanded { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05); }
.kanban-task:hover:not(.expanded) { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05); }
+.kanban-task.dragging { opacity: 0.4; transform: scale(1.02); box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04); cursor: grabbing; z-index: 100; }
.kanban-task-header { cursor: pointer; display: flex; flex-direction: column; gap: 0.5rem; }
.kanban-task-title {
@@ -312,6 +322,10 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
.role-from { background: rgba(var(--primary-rgb), 0.1); color: var(--primary); }
.role-to { background: rgba(16, 185, 129, 0.1); color: #10B981; }
+.kanban-task:not(.expanded) .expanded-only { display: none !important; }
+.kanban-task.expanded .avatar-collapsed-only { display: none !important; }
+.kanban-task.expanded .expanded-only { display: inline-flex !important; }
+
/* Custom Autocomplete Dropdown */
.autocomplete-dropdown {
position: absolute; top: 100%; left: 0; right: 0; background: var(--surface);
@@ -485,7 +499,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l