From 5984c18459b3d102ba0fd793864f7eec0ada365d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 15:49:23 +0700 Subject: [PATCH] test --- views/tasks.php | 1099 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1099 insertions(+) create mode 100644 views/tasks.php diff --git a/views/tasks.php b/views/tasks.php new file mode 100644 index 0000000..b2f0fea --- /dev/null +++ b/views/tasks.php @@ -0,0 +1,1099 @@ +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 DATE"); } 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) { + query("DELETE FROM tasks WHERE id=?", [$del_id]); + } +} + +// 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; + } + } + } + + $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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)"); + if(!$stmt->execute([$title, $desc, $user_id, $assignor_type, $assignor_group_id, $a_type, $a_id, $custom_name, $due_date])) { + 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"); + } + } + + 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'; + +$params = array_merge([$user_id], $my_group_ids, $my_group_ids, [$user_id]); + +$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 + 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) + 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 = ?) + ORDER BY t.created_at DESC"; +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$tasks = $stmt->fetchAll(); + +$t_ids = array_column($tasks, 'id'); +$attachments = []; +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; + } +} + +$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']) - strtotime($a['created_at']); + 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): ?> +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ + + +
+ + β–Ό +
+
+ +
+ '; + if($t['assignee_type'] == 'custom') echo 'πŸ‘€ '.htmlspecialchars((string)$t['custom_assignee_name']); + elseif($t['assignee_type'] == 'group') echo 'πŸ‘₯ '.htmlspecialchars($t['assignee_group_name']); + else echo 'πŸ‘€ '.htmlspecialchars($t['assignee_user_name'] ?? ''); + echo ''; + + echo '
'; + + $att_count = empty($attachments[$t['id']]) ? 0 : count($attachments[$t['id']]); + if($att_count > 0) { + echo ' '.$att_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 by '.abs($days_diff).'d'; + } elseif($days_diff == 0) { + echo 'Due Today'; + } else { + echo ''.$days_diff.'d left'; + } + } + echo '
'; + ?> +
+
+ +
+ +
+ + +
+
πŸ“… Created:
+ +
⏰ Due Date:
+ +
+ +
+
+ From: + + + +
+
+ To: + + + +
+
+ +
+
+
Attachments
+ +
+ + + +
+ +
+
+ + + +
+ +
+ +
πŸ“„
+ + +
+ + + + +
+
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +