From 97649d9a9857a02c6cd900ec79a3bfbcc5dc4f2d Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2026 21:38:48 +0700 Subject: [PATCH] add tag button --- index.php | 47 +++++++++++ views/tasks.php | 214 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 239 insertions(+), 22 deletions(-) diff --git a/index.php b/index.php index e9967df..5a444f3 100644 --- a/index.php +++ b/index.php @@ -187,6 +187,53 @@ if ($action === 'api') { 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; } diff --git a/views/tasks.php b/views/tasks.php index 234f166..db5677a 100644 --- a/views/tasks.php +++ b/views/tasks.php @@ -194,7 +194,7 @@ $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, cu.username as completed_by_username, aru.username as archived_by_username +$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 @@ -275,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, opacity 0.2s; + border: 1px solid rgba(0,0,0,0.05); 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); } @@ -288,7 +288,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l .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-header { cursor: grab; display: flex; flex-direction: column; gap: 0.5rem; } .kanban-task-title { font-weight: 700; font-size: 1.05rem; color: var(--text); /* Clamp to 2 lines when collapsed */ @@ -306,7 +306,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l -webkit-line-clamp: unset; } .kanban-task-body { - display: none; padding-top: 0.75rem; margin-top: 0.5rem; border-top: 1px dashed var(--border); + display: none; padding-top: 0.75rem; margin-top: 0.5rem; border-top: 1px dashed var(--border); position: relative; } .kanban-task.expanded .kanban-task-body { display: block; } .chevron { transition: transform 0.2s; font-size: 0.8rem; color: var(--text-light); } @@ -325,6 +325,8 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l .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; } +/* Hide header metadata row when expanded to avoid duplication */ +.kanban-task.expanded .kanban-task-header > div:last-child { display: none !important; } /* Custom Autocomplete Dropdown */ .autocomplete-dropdown { @@ -499,9 +501,9 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
-
+
-
+
@@ -524,14 +526,6 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
- -
- - - -
- -
'; - echo '
'.$to_initial.'
'; + if($to_profile_pic && file_exists($to_profile_pic)) { + echo ''; + } else { + echo '
'.$to_initial.'
'; + } echo '
'; echo ''; - echo '
'.$to_initial.'
'; + if($to_profile_pic && file_exists($to_profile_pic)) { + echo ''; + } else { + echo '
'.$to_initial.'
'; + } echo ''.$to_name.''; 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.''; @@ -601,8 +610,11 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l else $from_name = $t['assignor_user_name'] ?? 'System'; if (!function_exists('get_avatar_html_local')) { - function get_avatar_html_local($name) { + function get_avatar_html_local($name, $profile_pic = '') { if (!$name) return ''; + if($profile_pic && file_exists($profile_pic)) { + return ''; + } $initial = strtoupper(mb_substr($name, 0, 1)); $col_idx = crc32($name) % 8; $bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16']; @@ -610,18 +622,22 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l return '
'.$initial.'
'; } } + + // Get profile pics for expanded view + $from_profile_pic = ($t['assignor_type'] == 'user') ? ($t['assignor_profile_pic'] ?? '') : ''; + $to_profile_pic_expanded = ($t['assignee_type'] == 'user') ? ($t['assignee_profile_pic'] ?? '') : ''; ?>
Assignments:
From - +
|
To - +
@@ -643,6 +659,31 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
+ +
+
+
Tags
+ + + +
+
+ + + + + + + + + +
+
+
Comments
@@ -731,6 +772,16 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
+ + +
@@ -802,10 +853,13 @@ function allowDrop(ev) { function drag(ev, taskId) { ev.dataTransfer.setData("text", taskId); - setTimeout(() => ev.target.classList.add('dragging'), 0); + // Get the task element from the header + let taskEl = ev.target.closest('.kanban-task'); + setTimeout(() => taskEl.classList.add('dragging'), 0); } function dragEnd(ev) { - ev.target.classList.remove('dragging'); + let taskEl = ev.target.closest('.kanban-task'); + if(taskEl) taskEl.classList.remove('dragging'); } function sortPendingTasks() { @@ -1230,8 +1284,124 @@ function confirmCustomDelete(form, msg) { return false; } +function showAddTagPopup(taskId, existingTags) { + let tags = existingTags ? existingTags.split(',').filter(t => t.trim()) : []; + + let overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; z-index:9999; opacity:0; transition:opacity 0.2s;'; + + let modal = document.createElement('div'); + modal.style.cssText = 'background:var(--surface); padding:1.5rem; border-radius:1rem; min-width:300px; max-width:90vw; box-shadow:0 10px 25px rgba(0,0,0,0.2); transform:scale(0.9); transition:transform 0.2s;'; + + let tagsHtml = tags.map(tag => { + let escaped = tag.replace(/'/g, "\\'"); + return ` + ${tag} + + `; + }).join(''); + + modal.innerHTML = ` +

Manage Tags

+ +
+ + +
+
+ +
+ `; + + overlay.appendChild(modal); + document.body.appendChild(overlay); + + window.currentTagPopup = overlay; + window.currentTaskId = taskId; + + setTimeout(() => { + overlay.style.opacity = '1'; + modal.style.transform = 'scale(1)'; + document.getElementById('popup-tag-input').focus(); + }, 10); + + overlay.onclick = function(e) { + if(e.target === overlay) closeTagPopup(); + }; +} + +function closeTagPopup() { + if(window.currentTagPopup) { + window.currentTagPopup.style.opacity = '0'; + window.currentTagPopup.querySelector('div').style.transform = 'scale(0.9)'; + setTimeout(() => { + if(window.currentTagPopup) document.body.removeChild(window.currentTagPopup); + window.currentTagPopup = null; + window.location.reload(); + }, 200); + } +} + +function removeTagFromPopup(tag, taskId) { + fetch('?action=api&api_action=remove_task_tag', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `task_id=${taskId}&tag=${encodeURIComponent(tag)}` + }).then(r => r.json()).then(res => { + if(res.success) { + // Refresh popup + closeTagPopup(); + fetch('?action=api&api_action=get_tasks_status&task_id=' + taskId) + .then(r => r.json()) + .then(data => { + if(data && !data.status) { + showAddTagPopup(taskId, data.tags || ''); + } + }); + } + }); +} + +function addTagsFromPopup(taskId) { + let input = document.getElementById('popup-tag-input'); + let tags = input.value.split(',').map(t => t.trim()).filter(t => t); + if(!tags.length) return; + + let promises = tags.map(tag => + fetch('?action=api&api_action=add_task_tag', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `task_id=${taskId}&tag=${encodeURIComponent(tag)}` + }).then(r => r.json()) + ); + + Promise.all(promises).then(() => { + input.value = ''; + // Refresh popup + closeTagPopup(); + setTimeout(() => showAddTagPopup(taskId, ''), 250); + }); +} + +function addTaskTag(taskId) { + showAddTagPopup(taskId, ''); +} + +function removeTaskTag(taskId, tag) { + fetch('?action=api&api_action=remove_task_tag', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `task_id=${taskId}&tag=${encodeURIComponent(tag)}` + }).then(r => r.json()).then(res => { + if(res.success) { + window.location.reload(); + } else { + showToast('Error: ' + (res.error || 'Failed to remove tag')); + } + }); +} + function uploadFilesAjax(taskId, files) { - if(files.length === 0) return; let container = document.getElementById('progress-container-' + taskId); let fill = document.getElementById('progress-fill-' + taskId);