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 tags TEXT 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 system_config (k TEXT PRIMARY KEY, v TEXT)"); $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('completed_days', '7')"); $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('archived_days', '90')"); } catch(Exception $e) {} // Auto-cleanup lifecycle hooks $conf_comp = query("SELECT v FROM system_config WHERE k='completed_days'")->fetchColumn() ?: 7; $conf_arch = query("SELECT v FROM system_config WHERE k='archived_days'")->fetchColumn() ?: 90; if(is_numeric($conf_comp)) { $pdo->exec("UPDATE tasks SET status='archived', status_updated_at=CURRENT_TIMESTAMP WHERE status='completed' AND datetime(COALESCE(status_updated_at, created_at), '+$conf_comp days') <= CURRENT_TIMESTAMP"); } if(is_numeric($conf_arch)) { $pdo->exec("DELETE FROM tasks WHERE status='archived' AND datetime(COALESCE(status_updated_at, created_at), '+$conf_arch days') <= CURRENT_TIMESTAMP"); } // Handle Task Deletions if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_task'])) { $del_id = $_POST['delete_task_id']; $task_check = query("SELECT assignor_type, assignor_id, assignor_group_id FROM tasks WHERE id=?", [$del_id])->fetch(); $user_groups = get_user_groups($user_id); $my_group_ids = array_column($user_groups, 'id'); $can_delete = false; if($task_check) { if($task_check['assignor_type'] === 'user' && $task_check['assignor_id'] == $user_id) $can_delete = true; if($task_check['assignor_type'] === 'group' && in_array($task_check['assignor_group_id'], $my_group_ids)) $can_delete = true; if(!empty($_SESSION['is_admin'])) $can_delete = true; } if($can_delete) { $task_to_del = query("SELECT title FROM tasks WHERE id=?", [$del_id])->fetch(); query("DELETE FROM tasks WHERE id=?", [$del_id]); if($task_to_del) notify_waha('Deleted', 'Task', $task_to_del['title']); } } // Handle attachment deletion if($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['delete_attachment']) || isset($_POST['delete_attachment_ajax']))) { $att_id = $_POST['attachment_id']; $att = query("SELECT owner_id, file_path FROM task_attachments WHERE id=?", [$att_id])->fetch(); if($att && ($att['owner_id'] == $user_id || !empty($_SESSION['is_admin']))) { @unlink($att['file_path']); query("DELETE FROM task_attachments WHERE id=?", [$att_id]); } if(isset($_POST['delete_attachment_ajax'])) { echo "OK"; exit; } } // Handle AJAX file upload for existing tasks if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_attachment_ajax'])) { $task_id = (int)($_POST['task_id'] ?? 0); $task = query("SELECT id FROM tasks WHERE id=?", [$task_id])->fetch(); if($task && isset($_FILES['attachment_file'])) { $files = $_FILES['attachment_file']; $names = is_array($files['name']) ? $files['name'] : [$files['name']]; $tmps = is_array($files['tmp_name']) ? $files['tmp_name'] : [$files['tmp_name']]; $errors = is_array($files['error']) ? $files['error'] : [$files['error']]; for($i = 0; $i < count($names); $i++) { if($names[$i] && $errors[$i] === 0) { $ext = strtolower(pathinfo($names[$i], PATHINFO_EXTENSION)); $path = 'uploads/t_' . time() . '_' . rand(100, 999) . '.' . $ext; if(!is_dir('uploads')) mkdir('uploads'); if(move_uploaded_file($tmps[$i], $path)) { $s = $pdo->prepare("INSERT INTO task_attachments (task_id, file_name, file_path, owner_id) VALUES (?, ?, ?, ?)"); $s->execute([$task_id, $names[$i], $path, $user_id]); } } } echo json_encode(['success' => true]); exit; } echo json_encode(['success' => false, 'error' => 'Invalid task']); exit; } // Handle form submission if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_task'])) { $title = $_POST['title']; $desc = $_POST['description'] ?? ''; $due_date = $_POST['due_date'] ?? null; if(trim((string)$due_date) === '') $due_date = null; // Parse From $from = $_POST['from_assignor']; // 'user_1' or 'group_2' $f_parts = explode('_', $from); $assignor_type = $f_parts[0]; // 'user' or 'group' $assignor_group_id = ($assignor_type === 'group') ? $f_parts[1] : null; // Parse To Array (multi-assignee fallback to custom string) $to_raw = trim((string)($_POST['to_assignee'] ?? '')); $to_list = array_filter(array_map('trim', explode(',', $to_raw))); $a_type = 'custom'; $a_id = null; $custom_name = $to_raw; if(count($to_list) === 1) { $single = reset($to_list); $u = query("SELECT id FROM users WHERE username=?", [$single])->fetch(); if($u) { $a_type = 'user'; $a_id = $u['id']; $custom_name = null; } else { $g = query("SELECT id FROM groups WHERE name=?", [$single])->fetch(); if($g) { $a_type = 'group'; $a_id = $g['id']; $custom_name = null; } } } $tags = trim($_POST['tags'] ?? ''); $stmt = $pdo->prepare("INSERT INTO tasks (title, description, assignor_id, assignor_type, assignor_group_id, assignee_type, assignee_id, custom_assignee_name, status_updated_at, due_date, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)"); if(!$stmt->execute([$title, $desc, $user_id, $assignor_type, $assignor_group_id, $a_type, $a_id, $custom_name, $due_date, $tags])) { if(isset($_POST['ajax_submit'])) { echo json_encode(['success'=>false, 'error'=>'Task insert failed']); exit; } } $new_task_id = $pdo->lastInsertId(); // Process attachments bundled in this single request if(isset($_FILES['attachment_file'])) { $files = $_FILES['attachment_file']; $names = is_array($files['name']) ? $files['name'] : [$files['name']]; $tmps = is_array($files['tmp_name']) ? $files['tmp_name'] : [$files['tmp_name']]; $errors = is_array($files['error']) ? $files['error'] : [$files['error']]; for($i=0; $iprepare("INSERT INTO task_attachments (task_id, file_name, file_path, owner_id) VALUES (?, ?, ?, ?)"); $stmt->execute([$new_task_id, $names[$i], $path, $user_id]); } } } } // Handle notifications foreach($to_list as $rec) { $u = query("SELECT id FROM users WHERE username=?", [$rec])->fetch(); if($u) notify_user($u['id'], "You have been assigned a new task: $title"); else { $g = query("SELECT id FROM groups WHERE name=?", [$rec])->fetch(); if($g) notify_group($g['id'], "Your group has been assigned a new task: $title"); } } notify_waha('Created', 'Task', $title); if(isset($_POST['ajax_submit'])) { echo json_encode(['success'=>true, 'redirect'=>'?action=tasks&msg_title='.urlencode('Task Created').'&msg_body='.urlencode('Task successfully created.')]); exit; } header('Location: ?action=tasks'); exit; } // Fetch Assignable Entities for "From" if(!isset($user_groups)) $user_groups = get_user_groups($user_id); if(!isset($my_group_ids)) $my_group_ids = array_column($user_groups, 'id'); $from_options = []; $from_options[] = ['val' => 'user_'.$user_id, 'label' => 'Myself ('.htmlspecialchars($current_user['username']).')']; foreach($user_groups as $g) { $from_options[] = ['val' => 'group_'.$g['id'], 'label' => htmlspecialchars($g['name']) . ' (Group)']; } $all_u = query("SELECT id, username FROM users")->fetchAll(); $all_g = query("SELECT id, name FROM groups")->fetchAll(); $group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0'; $custom_tokens = []; if(!empty($current_user['username'])) $custom_tokens[] = (string)$current_user['username']; foreach($user_groups as $g) { if(!empty($g['name'])) $custom_tokens[] = (string)$g['name']; } $custom_tokens = array_values(array_unique(array_filter(array_map('trim', $custom_tokens), fn($v) => $v !== ''))); $custom_in = implode(',', array_fill(0, count($custom_tokens), '?')) ?: '0'; $custom_like = $custom_tokens ? implode(' OR ', array_fill(0, count($custom_tokens), "(','||lower(replace(t.custom_assignee_name,' ',''))||',') LIKE ('%,'||lower(replace(?,' ',''))||',%')")) : '0'; $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, u.profile_pic as assignor_profile_pic, g.name as assignor_group_name, au.username as assignee_user_name, au.profile_pic as assignee_profile_pic, 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)) OR (t.assignor_type='user' AND t.assignor_id = ?) OR ( COALESCE(t.assignee_type,'custom')='custom' AND t.custom_assignee_name IS NOT NULL AND ( t.custom_assignee_name IN ($custom_in) OR ($custom_like) ) ) ORDER BY t.created_at DESC"; $stmt = $pdo->prepare($sql); $stmt->execute($params); $tasks = $stmt->fetchAll(); $t_ids = array_column($tasks, 'id'); $attachments = []; $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"); $stmt->execute($t_ids); foreach($stmt->fetchAll() as $row) { $attachments[$row['task_id']][] = $row; } $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) { $comments[$row['task_id']][] = $row; } } // Pre-fetch profile pics for all potential assignees in the view $all_usernames = []; foreach($tasks as $t) { if($t['assignee_type'] == 'user' || $t['assignee_type'] == 'custom') { $names = array_filter(array_map('trim', explode(',', $t['assignee_user_name'] ?? $t['custom_assignee_name'] ?? ''))); foreach($names as $n) $all_usernames[] = $n; } if($t['assignor_type'] == 'user') $all_usernames[] = $t['assignor_user_name']; } $user_pics = []; if($all_usernames) { $all_usernames = array_unique($all_usernames); $in_u = str_repeat('?,', count($all_usernames) - 1) . '?'; $stmt_u = $pdo->prepare("SELECT username, profile_pic FROM users WHERE username IN ($in_u)"); $stmt_u->execute(array_values($all_usernames)); foreach($stmt_u->fetchAll() as $row) { $user_pics[$row['username']] = $row['profile_pic']; } } $kanban = ['pending' => [], 'in_progress' => [], 'completed' => [], 'archived' => []]; foreach($tasks as $t) { if(isset($kanban[$t['status']])) $kanban[$t['status']][] = $t; } // Sort Pending by Due Date (Earliest first) usort($kanban['pending'], function($a, $b) { $da = $a['due_date'] ? strtotime($a['due_date']) : PHP_INT_MAX; $db = $b['due_date'] ? strtotime($b['due_date']) : PHP_INT_MAX; if($da == $db) return strtotime($b['created_at'] . ' UTC') - strtotime($a['created_at'] . ' UTC'); return $da - $db; }); ?>
Preview
πŸ“
'#F59E0B','in_progress'=>'#3B82F6','completed'=>'#10B981','archived'=>'#6B7280']; foreach(['pending'=>['Pending','🟠'], 'in_progress'=>['In Progress','πŸ”΅'], 'completed'=>['Completed','🟒'], 'archived'=>['Archived','πŸ—„οΈ']] as $col_id => $col_data): ?>
β–Ό
{assignee} echo '
'; // From Avatar if ($from_pic && file_exists($from_pic)) { echo ''; } else { echo '
'.$from_initial.'
'; } // Arrow style separator echo ''; // To Avatar(s) stack echo '
'; foreach($to_names as $idx => $name) { $initial = strtoupper(mb_substr($name, 0, 1)); $col_idx = crc32($name) % 8; $av_bg = $bg_colors[$col_idx]; $pic = $user_pics[$name] ?? ''; $margin = ($idx === 0) ? '0' : '-8px'; if ($pic && file_exists($pic)) { echo ''; } else { echo '
'.$initial.'
'; } } echo '
'; echo '
'; // Expanded View Badges echo '
'; foreach($to_names as $name) { $initial = strtoupper(mb_substr($name, 0, 1)); $col_idx = crc32($name) % 8; $av_bg = $bg_colors[$col_idx]; $pic = $user_pics[$name] ?? ''; echo ''; if ($pic && file_exists($pic)) { echo ''; } else { echo '
'.$initial.'
'; } echo ''.htmlspecialchars($name).''; echo '
'; } echo '
'; echo '
'; $tag_count = !empty($t['tags']) ? count(array_filter(array_map('trim', explode(',', $t['tags'])))) : 0; if($tag_count > 0) { echo ' '.$tag_count.''; } $att_count = empty($attachments[$t['id']]) ? 0 : count($attachments[$t['id']]); if($att_count > 0) { echo ' '.$att_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']) { $days_diff = (int)round((strtotime($t['due_date']) - strtotime(date('Y-m-d'))) / 86400); if($days_diff < 0) { echo 'Overdue '.abs($days_diff).'d ago'; } elseif($days_diff == 0) { echo 'Due Today'; } else { echo ''.$days_diff.'d left'; } } echo '
'; ?>
'; } $initial = strtoupper(mb_substr($name, 0, 1)); $col_idx = crc32($name) % 8; $bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16']; $av_bg = $bg_colors[$col_idx]; return '
'.$initial.'
'; } } $from_profile_pic = ($t['assignor_type'] == 'user') ? ($user_pics[$from_name] ?? '') : ''; ?>
Assignments:
From
|
To
Completed by:
Archived by:
Comments
 
No comments yet.
πŸ“„