exec("ALTER TABLE tasks ADD COLUMN assignor_type TEXT DEFAULT 'user'"); } catch(Exception $e) {} try { $pdo->exec("ALTER TABLE tasks ADD COLUMN assignor_group_id INTEGER"); } catch(Exception $e) {} 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 DATETIME"); } 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_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; if(!$value) return null; $ts = strtotime((string)$value); if($ts === false) return null; return date('Y-m-d H:i', $ts); }; $format_status = function($value) { if(!$value) return null; $s = strtolower(trim((string)$value)); if($s === 'in_progress') return 'In Progress'; $s = str_replace('_', ' ', $s); return ucfirst($s); }; if($api_action === 'update_task_status') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); $task_id = $_POST['task_id'] ?? null; $status = $_POST['status'] ?? null; $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); $t = query("SELECT title, assignee_type, assignee_id FROM tasks WHERE id=?", [$task_id])->fetch(); if($t) { $ctx = ['action' => 'Moved to '.$status, 'module' => 'Task', 'item' => $t['title']]; if($t['assignee_type'] === 'user' && $t['assignee_id']) { notify_user($t['assignee_id'], "Task '{$t['title']}' moved to $status", $ctx); } elseif($t['assignee_type'] === 'group' && $t['assignee_id']) { notify_group($t['assignee_id'], "Group task '{$t['title']}' moved to $status", $ctx); } } echo json_encode(['status'=>'ok']); exit; } if($api_action === 'add_comment') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); $task_id = $_POST['task_id'] ?? null; $comment = $_POST['comment'] ?? null; $user_id = $_SESSION['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, assignee_type, assignee_id FROM tasks WHERE id=?", [$task_id])->fetch(); if($t) { $ctx = ['action' => 'New Comment', 'module' => 'Task', 'item' => $t['title']]; if($t['assignee_type'] === 'user' && $t['assignee_id']) { notify_user($t['assignee_id'], "New comment on '{$t['title']}'", $ctx); } elseif($t['assignee_type'] === 'group' && $t['assignee_id']) { notify_group($t['assignee_id'], "New comment on group task '{$t['title']}'", $ctx); } } } echo json_encode(['status'=>'ok']); exit; } if($api_action === 'delete_comment') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); $comment_id = $_POST['comment_id'] ?? null; if($comment_id) { $user_id = $_SESSION['user_id']; $is_admin = !empty($_SESSION['is_admin']); $comment_check = query("SELECT created_by FROM task_comments WHERE id = ?", [$comment_id])->fetch(); if($comment_check && ($comment_check['created_by'] == $user_id || $is_admin)) { $stmt = $pdo->prepare("DELETE FROM task_comments WHERE id = ?"); $stmt->execute([$comment_id]); } else { echo json_encode(['status'=>'error','msg'=>'forbidden']); exit; } } echo json_encode(['status'=>'ok']); exit; } if($api_action === 'get_tasks_status') { if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized'])); $task_id = $_GET['task_id'] ?? null; // Correlated subquery gives attachment count per task. $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, (SELECT COUNT(*) FROM task_attachments ta WHERE ta.task_id = t.id) AS attachments_count, (SELECT COUNT(*) FROM task_comments tc WHERE tc.task_id = t.id) AS comments_count, (SELECT comment FROM task_comments tc WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_comment, (SELECT created_at FROM task_comments tc WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_comment_date, (SELECT cu.username FROM task_comments tc JOIN users cu ON tc.created_by = cu.id WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_commenter, t.status_updated_at, t.completed_by, t.archived_by, t.tags, t.title 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) "; $params = []; if($task_id !== null && $task_id !== '') { if(!ctype_digit((string)$task_id)) { echo json_encode(['status'=>'error','msg'=>'Invalid task_id']); exit; } $sql .= " WHERE t.id = ? LIMIT 1"; $params[] = (int)$task_id; } else { $sql .= " ORDER BY t.created_at DESC"; } $stmt = $pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); if($task_id !== null && $task_id !== '') { $row = $rows ? $rows[0] : null; if(!$row) { echo json_encode(['status'=>'error','msg'=>'Task not found']); exit; } $from = null; if(($row['assignor_type'] ?? null) === 'group') $from = $row['assignor_group_name'] ?? null; elseif(($row['assignor_type'] ?? null) === 'user') $from = $row['assignor_user_name'] ?? null; $to = null; if(($row['assignee_type'] ?? null) === 'user') $to = $row['assignee_user_name'] ?? null; elseif(($row['assignee_type'] ?? null) === 'group') $to = $row['assignee_group_name'] ?? null; elseif(($row['assignee_type'] ?? null) === 'custom') $to = $row['custom_assignee_name'] ?? null; $desc = (string)($row['description'] ?? ''); $desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? ''); $tags = !empty($row['tags']) ? array_filter(array_map('trim', explode(',', $row['tags']))) : []; // Logic for last_modified_by // If it's archived, 'Archived by' user; if completed, 'Completed by' user; otherwise latest commenter or creator. $last_mod_by = 'System'; if ($row['status'] === 'archived' && $row['archived_by']) { $last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['archived_by']])->fetchColumn() ?: 'System'; } elseif ($row['status'] === 'completed' && $row['completed_by']) { $last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['completed_by']])->fetchColumn() ?: 'System'; } else { $last_mod_by = $row['last_commenter'] ?: $row['assignor_user_name']; } $payload = [ 'task_id' => (string)$row['id'], 'task_name' => (string)$row['title'], 'task_created_date' => $format_dt($row['created_at'] ? $row['created_at'] . ' UTC' : null), 'from' => $from, 'to' => $to, 'task_description' => $desc, 'due_date' => $format_dt($row['due_date'] ?? null), 'current_status' => $format_status($row['status'] ?? null), 'attachments_count' => (int)($row['attachments_count'] ?? 0), 'tags_count' => count($tags), 'comments_count' => (int)($row['comments_count'] ?? 0), 'last_modified_by' => $last_mod_by, 'last_modified_date' => $format_dt(($row['status_updated_at'] ?: $row['created_at']) . ' UTC'), 'last_comment' => $row['last_comment'] ?: null, 'last_comment_date' => $format_dt($row['last_comment_date'] ? $row['last_comment_date'] . ' UTC' : null), ]; echo json_encode($payload); exit; } $result = []; foreach($rows as $row) { $from = null; if(($row['assignor_type'] ?? null) === 'group') $from = $row['assignor_group_name'] ?? null; elseif(($row['assignor_type'] ?? null) === 'user') $from = $row['assignor_user_name'] ?? null; $to = null; if(($row['assignee_type'] ?? null) === 'user') $to = $row['assignee_user_name'] ?? null; elseif(($row['assignee_type'] ?? null) === 'group') $to = $row['assignee_group_name'] ?? null; elseif(($row['assignee_type'] ?? null) === 'custom') $to = $row['custom_assignee_name'] ?? null; $desc = (string)($row['description'] ?? ''); $desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? ''); $tags = !empty($row['tags']) ? array_filter(array_map('trim', explode(',', $row['tags']))) : []; $last_mod_by = 'System'; if ($row['status'] === 'archived' && $row['archived_by']) { $last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['archived_by']])->fetchColumn() ?: 'System'; } elseif ($row['status'] === 'completed' && $row['completed_by']) { $last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['completed_by']])->fetchColumn() ?: 'System'; } else { $last_mod_by = $row['last_commenter'] ?: $row['assignor_user_name']; } $result[] = [ 'task_id' => (string)$row['id'], 'task_name' => (string)$row['title'], 'task_created_date' => $format_dt($row['created_at'] ? $row['created_at'] . ' UTC' : null), 'from' => $from, 'to' => $to, 'task_description' => $desc, 'due_date' => $format_dt($row['due_date'] ?? null), 'current_status' => $format_status($row['status'] ?? null), 'attachments_count' => (int)($row['attachments_count'] ?? 0), 'tags_count' => count($tags), 'comments_count' => (int)($row['comments_count'] ?? 0), 'last_modified_by' => $last_mod_by, 'last_modified_date' => $format_dt(($row['status_updated_at'] ?: $row['created_at']) . ' UTC'), 'last_comment' => $row['last_comment'] ?: null, 'last_comment_date' => $format_dt($row['last_comment_date'] ? $row['last_comment_date'] . ' UTC' : null), ]; } echo json_encode($result); exit; } if($api_action === 'add_task_tag') { if(!isset($_SESSION['user_id'])) die(json_encode(['success'=>false,'error'=>'unauthorized'])); $task_id = $_POST['task_id'] ?? null; $tag = trim($_POST['tag'] ?? ''); $user_id = $_SESSION['user_id']; if($task_id && $tag) { $stmt = $pdo->prepare("SELECT tags FROM tasks WHERE id = ?"); $stmt->execute([$task_id]); $row = $stmt->fetch(); $current_tags = $row ? ($row['tags'] ?? '') : ''; $tags_array = $current_tags ? array_filter(array_map('trim', explode(',', $current_tags))) : []; if(!in_array($tag, $tags_array)) { $tags_array[] = $tag; $new_tags = implode(',', $tags_array); $pdo->prepare("UPDATE tasks SET tags = ? WHERE id = ?")->execute([$new_tags, $task_id]); } echo json_encode(['success'=>true]); } else { echo json_encode(['success'=>false,'error'=>'Missing task_id or tag']); } exit; } if($api_action === 'remove_task_tag') { if(!isset($_SESSION['user_id'])) die(json_encode(['success'=>false,'error'=>'unauthorized'])); $task_id = $_POST['task_id'] ?? null; $tag = trim($_POST['tag'] ?? ''); if($task_id && $tag) { $stmt = $pdo->prepare("SELECT tags FROM tasks WHERE id = ?"); $stmt->execute([$task_id]); $row = $stmt->fetch(); $current_tags = $row ? ($row['tags'] ?? '') : ''; $tags_array = $current_tags ? array_filter(array_map('trim', explode(',', $current_tags))) : []; $tags_array = array_diff($tags_array, [$tag]); $new_tags = implode(',', $tags_array); $pdo->prepare("UPDATE tasks SET tags = ? WHERE id = ?")->execute([$new_tags, $task_id]); echo json_encode(['success'=>true]); } else { echo json_encode(['success'=>false,'error'=>'Missing task_id or tag']); } exit; } echo json_encode(['status'=>'error','msg'=>'Unknown api_action']); exit; } if (!isset($_SESSION['user_id']) && $action !== 'login' && $action !== 'setup') { header('Location: ?action=login'); exit; } $user_id = $_SESSION['user_id'] ?? null; $current_user = null; $unread_notifications = 0; if ($user_id) { $current_user = get_user($user_id); $stmt = $pdo->prepare("SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0"); $stmt->execute([$user_id]); $unread_notifications = $stmt->fetchColumn(); } if ($action === 'logout') { session_destroy(); header('Location: ?action=login'); exit; } if ($action === 'setup') { include 'setup.php'; exit; } if ($action === 'login') { $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim((string)($_POST['username'] ?? '')); $password = trim((string)($_POST['password'] ?? '')); $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['user_id'] = $user['id']; $_SESSION['is_admin'] = $user['is_admin']; header('Location: ?action=dashboard'); exit; } else { $error = "Invalid username or password"; } } include 'views/login.php'; exit; } // Handle POST actions that need a redirect BEFORE layout outputs any HTML. if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user_id) { // Mood setter if (isset($_POST['set_mood'])) { try { $pdo->exec("ALTER TABLE users ADD COLUMN mood TEXT DEFAULT NULL"); } catch(Exception $e) {} try { $pdo->exec("ALTER TABLE users ADD COLUMN mood_updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {} try { $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT NOT NULL, detail TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)"); } catch(Exception $e) {} $mood = trim((string)($_POST['mood'] ?? '')); $allowed = ['😊','😢','😠','😴','🤩','😟','😎','🤔','😐','🥳']; if (in_array($mood, $allowed) || $mood === '') { $pdo->prepare("UPDATE users SET mood=?, mood_updated_at=CURRENT_TIMESTAMP WHERE id=?") ->execute([$mood ?: null, $user_id]); try { $pdo->prepare("INSERT INTO activity_log (user_id, action, detail) VALUES (?, 'mood_set', ?)") ->execute([$user_id, $mood ?: 'cleared']); } catch(Exception $e) {} } header('Location: ?action=dashboard'); exit; } // Profile picture upload if (isset($_POST['upload_profile_pic'])) { // Ensure column exists try { $pdo->exec("ALTER TABLE users ADD COLUMN profile_pic TEXT DEFAULT NULL"); } catch(Exception $e) {} if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === 0) { $allowed_types = ['image/jpeg','image/png','image/gif','image/webp']; if (in_array($_FILES['profile_pic']['type'], $allowed_types)) { $ext = pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION); $filename = 'avatar_' . $user_id . '_' . time() . '.' . $ext; $dest = 'uploads/' . $filename; if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $dest)) { // Delete old pic $old = $pdo->prepare("SELECT profile_pic FROM users WHERE id=?"); $old->execute([$user_id]); $old_pic = $old->fetchColumn(); if ($old_pic && file_exists($old_pic)) @unlink($old_pic); $pdo->prepare("UPDATE users SET profile_pic=? WHERE id=?")->execute([$dest, $user_id]); } } } header('Location: ?action=' . ($_POST['redirect_to'] ?? 'dashboard')); exit; } // Delete profile picture if (isset($_POST['delete_profile_pic'])) { try { $pdo->exec("ALTER TABLE users ADD COLUMN profile_pic TEXT DEFAULT NULL"); } catch(Exception $e) {} $old = $pdo->prepare("SELECT profile_pic FROM users WHERE id=?"); $old->execute([$user_id]); $old_pic = $old->fetchColumn(); if ($old_pic && file_exists($old_pic)) @unlink($old_pic); $pdo->prepare("UPDATE users SET profile_pic=NULL WHERE id=?")->execute([$user_id]); header('Location: ?action=' . ($_POST['redirect_to'] ?? 'dashboard')); exit; } // Mark all notifications as read if (isset($_POST['mark_all_read'])) { $pdo->prepare("UPDATE notifications SET is_read=1 WHERE user_id=?")->execute([$user_id]); header('Location: ?action=' . ($_POST['redirect_to'] ?? 'dashboard')); exit; } // Change password if (isset($_POST['change_password'])) { $cur = trim($_POST['current_password'] ?? ''); $new = trim($_POST['new_password'] ?? ''); $cnf = trim($_POST['confirm_password'] ?? ''); $user = get_user($user_id); $pw_error = ''; if (!password_verify($cur, $user['password_hash'])) { $pw_error = 'Current password is incorrect.'; } elseif (strlen($new) < 6) { $pw_error = 'New password must be at least 6 characters.'; } elseif ($new !== $cnf) { $pw_error = 'Passwords do not match.'; } else { $pdo->prepare("UPDATE users SET password_hash=? WHERE id=?") ->execute([password_hash($new, PASSWORD_DEFAULT), $user_id]); } $_SESSION['pw_change_msg'] = $pw_error ?: 'success'; header('Location: ?action=' . ($_POST['redirect_to'] ?? 'dashboard')); exit; } } $view_file = 'views/' . basename($action) . '.php'; if (file_exists($view_file)) { $is_ajax_post = isset($_POST['ajax_submit']) || isset($_POST['delete_attachment_ajax']) || isset($_POST['upload_attachment_ajax']); if ($is_ajax_post) { include $view_file; } else { include 'views/layout_top.php'; include $view_file; include 'views/layout_bottom.php'; } } else { echo "404 Not Found"; }