add tag button

This commit is contained in:
root
2026-03-29 21:38:48 +07:00
parent 513de7813f
commit 97649d9a98
2 changed files with 239 additions and 22 deletions

View File

@@ -187,6 +187,53 @@ if ($action === 'api') {
exit; 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']); echo json_encode(['status'=>'error','msg'=>'Unknown api_action']);
exit; exit;
} }

View File

@@ -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); $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 FROM tasks t
LEFT JOIN users u ON t.assignor_id = u.id LEFT JOIN users u ON t.assignor_id = u.id
LEFT JOIN groups g ON t.assignor_group_id = g.id LEFT JOIN groups g ON t.assignor_group_id = g.id
@@ -275,7 +275,7 @@ usort($kanban['pending'], function($a, $b) {
.kanban-task { .kanban-task {
background: var(--surface); padding: 1rem; border-radius: 10px; 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); 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); 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); } 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: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.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 { .kanban-task-title {
font-weight: 700; font-size: 1.05rem; color: var(--text); font-weight: 700; font-size: 1.05rem; color: var(--text);
/* Clamp to 2 lines when collapsed */ /* 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; -webkit-line-clamp: unset;
} }
.kanban-task-body { .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; } .kanban-task.expanded .kanban-task-body { display: block; }
.chevron { transition: transform 0.2s; font-size: 0.8rem; color: var(--text-light); } .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:not(.expanded) .expanded-only { display: none !important; }
.kanban-task.expanded .avatar-collapsed-only { display: none !important; } .kanban-task.expanded .avatar-collapsed-only { display: none !important; }
.kanban-task.expanded .expanded-only { display: inline-flex !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 */ /* Custom Autocomplete Dropdown */
.autocomplete-dropdown { .autocomplete-dropdown {
@@ -499,9 +501,9 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
</div> </div>
<div class="kanban-tasks" id="col-<?= $col_id ?>"> <div class="kanban-tasks" id="col-<?= $col_id ?>">
<?php foreach($kanban[$col_id] as $t): ?> <?php foreach($kanban[$col_id] as $t): ?>
<div class="kanban-task status-<?= $col_id ?>" draggable="true" ondragstart="drag(event, <?= $t['id'] ?>)" ondragend="dragEnd(event)" id="task-<?= $t['id'] ?>" data-due="<?= $t['due_date'] ? strtotime($t['due_date']) : PHP_INT_MAX ?>" data-created="<?= strtotime($t['created_at']) ?>"> <div class="kanban-task status-<?= $col_id ?>" id="task-<?= $t['id'] ?>" data-due="<?= $t['due_date'] ? strtotime($t['due_date']) : PHP_INT_MAX ?>" data-created="<?= strtotime($t['created_at']) ?>">
<div class="kanban-task-header" onclick="toggleTaskExpand(this)"> <div class="kanban-task-header" draggable="true" ondragstart="drag(event, <?= $t['id'] ?>)" ondragend="dragEnd(event)" onclick="toggleTaskExpand(this)">
<div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%; min-width:0; gap:0.5rem;"> <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%; min-width:0; gap:0.5rem;">
<span class="kanban-task-title"><?= htmlspecialchars($t['title']) ?></span> <span class="kanban-task-title"><?= htmlspecialchars($t['title']) ?></span>
<div style="display:flex; align-items:center; gap:0.5rem; flex-shrink:0;"> <div style="display:flex; align-items:center; gap:0.5rem; flex-shrink:0;">
@@ -524,14 +526,6 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
</div> </div>
</div> </div>
<?php if(!empty($t['tags'])): ?>
<div style="display:flex; flex-wrap:wrap; gap:0.25rem; margin-top:0.5rem;">
<?php foreach(explode(',', $t['tags']) as $tag): $tag=trim($tag); if(!$tag) continue; ?>
<span style="font-size:0.65rem; padding:2px 8px; border-radius:12px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-weight:600;"><i class="fa fa-tag" style="font-size:0.6rem;"></i> <?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; margin-top:0.5rem;"> <div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; margin-top:0.5rem;">
<?php <?php
$to_name = ''; $to_name = '';
@@ -539,22 +533,37 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
elseif($t['assignee_type'] == 'group') $to_name = htmlspecialchars($t['assignee_group_name']); elseif($t['assignee_type'] == 'group') $to_name = htmlspecialchars($t['assignee_group_name']);
else $to_name = htmlspecialchars($t['assignee_user_name'] ?? ''); else $to_name = htmlspecialchars($t['assignee_user_name'] ?? '');
// Get assignee profile pic for user assignments
$to_profile_pic = ($t['assignee_type'] == 'user') ? ($t['assignee_profile_pic'] ?? '') : '';
$to_initial = strtoupper(mb_substr($to_name, 0, 1)); $to_initial = strtoupper(mb_substr($to_name, 0, 1));
$col_idx = crc32($to_name) % 8; $col_idx = crc32($to_name) % 8;
$bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16']; $bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
$av_bg = $bg_colors[$col_idx]; $av_bg = $bg_colors[$col_idx];
echo '<div class="avatar-collapsed-only" style="display:flex; align-items:center;" title="To: ' . $to_name . '">'; echo '<div class="avatar-collapsed-only" style="display:flex; align-items:center;" title="To: ' . $to_name . '">';
echo '<div style="width:24px; height:24px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.75rem; border:2px solid var(--surface); margin-right:-8px; z-index:1;">'.$to_initial.'</div>'; if($to_profile_pic && file_exists($to_profile_pic)) {
echo '<img src="'.htmlspecialchars($to_profile_pic).'" style="width:24px; height:24px; border-radius:50%; object-fit:cover; border:2px solid var(--surface); margin-right:-8px; z-index:1;">';
} else {
echo '<div style="width:24px; height:24px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.75rem; border:2px solid var(--surface); margin-right:-8px; z-index:1;">'.$to_initial.'</div>';
}
echo '</div>'; echo '</div>';
echo '<span class="badge-role role-to expanded-only" style="padding:2px 8px; align-items:center; gap:0.4rem;">'; echo '<span class="badge-role role-to expanded-only" style="padding:2px 8px; align-items:center; gap:0.4rem;">';
echo '<div style="width:18px; height:18px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.6rem;">'.$to_initial.'</div>'; if($to_profile_pic && file_exists($to_profile_pic)) {
echo '<img src="'.htmlspecialchars($to_profile_pic).'" style="width:18px; height:18px; border-radius:50%; object-fit:cover;">';
} else {
echo '<div style="width:18px; height:18px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.6rem;">'.$to_initial.'</div>';
}
echo '<span>'.$to_name.'</span>'; echo '<span>'.$to_name.'</span>';
echo '</span>'; echo '</span>';
echo '<div style="display:flex; align-items:center; gap:0.5rem; margin-left:auto;">'; echo '<div style="display:flex; align-items:center; gap:0.5rem; margin-left:auto;">';
$tag_count = !empty($t['tags']) ? count(array_filter(array_map('trim', explode(',', $t['tags'])))) : 0;
if($tag_count > 0) {
echo '<span style="color:var(--primary); font-weight:600; display:flex; align-items:center; gap:0.15rem;" title="'.$tag_count.' tag(s)"><i class="fas fa-tag" style="font-size:0.75rem;"></i> '.$tag_count.'</span>';
}
$att_count = empty($attachments[$t['id']]) ? 0 : count($attachments[$t['id']]); $att_count = empty($attachments[$t['id']]) ? 0 : count($attachments[$t['id']]);
if($att_count > 0) { if($att_count > 0) {
echo '<span style="color:var(--text-light); font-weight:600; display:flex; align-items:center; gap:0.15rem;" title="'.$att_count.' attachment(s)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg> '.$att_count.'</span>'; echo '<span style="color:var(--text-light); font-weight:600; display:flex; align-items:center; gap:0.15rem;" title="'.$att_count.' attachment(s)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg> '.$att_count.'</span>';
@@ -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'; else $from_name = $t['assignor_user_name'] ?? 'System';
if (!function_exists('get_avatar_html_local')) { 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 (!$name) return '';
if($profile_pic && file_exists($profile_pic)) {
return '<img src="'.htmlspecialchars($profile_pic).'" style="width:24px; height:24px; border-radius:50%; object-fit:cover; border:2px solid var(--surface); cursor:help;" title="'.htmlspecialchars($name).'">';
}
$initial = strtoupper(mb_substr($name, 0, 1)); $initial = strtoupper(mb_substr($name, 0, 1));
$col_idx = crc32($name) % 8; $col_idx = crc32($name) % 8;
$bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16']; $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 '<div style="width:24px; height:24px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.75rem; border:2px solid var(--surface); cursor:help;" title="'.htmlspecialchars($name).'">'.$initial.'</div>'; return '<div style="width:24px; height:24px; border-radius:50%; background:'.$av_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.75rem; border:2px solid var(--surface); cursor:help;" title="'.htmlspecialchars($name).'">'.$initial.'</div>';
} }
} }
// 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'] ?? '') : '';
?> ?>
<div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;"> <div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;">
<span style="color:var(--text-light);">Assignments:</span> <span style="color:var(--text-light);">Assignments:</span>
<div style="display:flex; align-items:center; gap:0.5rem;"> <div style="display:flex; align-items:center; gap:0.5rem;">
<div style="display:flex; align-items:center; gap:0.4rem;"> <div style="display:flex; align-items:center; gap:0.4rem;">
<span style="font-size:0.65rem; color:var(--text-light);">From</span> <span style="font-size:0.65rem; color:var(--text-light);">From</span>
<?= get_avatar_html_local($from_name) ?> <?= get_avatar_html_local($from_name, $from_profile_pic) ?>
</div> </div>
<div style="color:var(--border);">|</div> <div style="color:var(--border);">|</div>
<div style="display:flex; align-items:center; gap:0.4rem;"> <div style="display:flex; align-items:center; gap:0.4rem;">
<span style="font-size:0.65rem; color:var(--text-light);">To</span> <span style="font-size:0.65rem; color:var(--text-light);">To</span>
<?= get_avatar_html_local($to_name) ?> <?= get_avatar_html_local($to_name, $to_profile_pic_expanded) ?>
</div> </div>
</div> </div>
</div> </div>
@@ -643,6 +659,31 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
<?php endif; ?> <?php endif; ?>
</div> </div>
<!-- Tags Section for Expanded View -->
<div style="margin-top:0.75rem; border-top:1px dashed var(--border); padding-top:0.75rem;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
<div style="font-size:0.75rem; font-weight:600; color:var(--text-light);">Tags</div>
<?php if(!empty($task_tags)): ?>
<button type="button" onclick="showAddTagPopup(<?= $t['id'] ?>)" style="background:var(--primary); color:white; border:none; border-radius:4px; width:20px; height:20px; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:0.7rem;" title="Add Tag"><i class="fas fa-plus"></i></button>
<?php endif; ?>
</div>
<div id="tags-container-<?= $t['id'] ?>" style="display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:0.5rem; align-items:center;">
<?php
$task_tags = !empty($t['tags']) ? array_filter(array_map('trim', explode(',', $t['tags']))) : [];
foreach($task_tags as $tag):
$tag_escaped = htmlspecialchars(addslashes($tag));
?>
<span class="task-tag" style="font-size:0.7rem; padding:3px 10px; border-radius:12px; background:rgba(var(--primary-rgb),0.15); color:var(--primary); font-weight:600; display:flex; align-items:center; gap:0.3rem;">
<i class="fas fa-tag" style="font-size:0.6rem;"></i> <?= htmlspecialchars($tag) ?>
<i class="fas fa-times" style="cursor:pointer; opacity:0.6; font-size:0.65rem;" onclick="removeTaskTag(<?= $t['id'] ?>, '<?= $tag_escaped ?>')" title="Remove tag"></i>
</span>
<?php endforeach; ?>
<?php if(empty($task_tags)): ?>
<button type="button" onclick="showAddTagPopup(<?= $t['id'] ?>)" style="background:var(--primary); color:white; border:none; border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:0.75rem;" title="Add Tag"><i class="fas fa-plus"></i></button>
<?php endif; ?>
</div>
</div>
<div style="margin-top:0.75rem; border-top:1px dashed var(--border); padding-top:0.75rem;"> <div style="margin-top:0.75rem; border-top:1px dashed var(--border); padding-top:0.75rem;">
<div style="font-size:0.75rem; font-weight:600; color:var(--text-light); margin-bottom:0.5rem;">Comments</div> <div style="font-size:0.75rem; font-weight:600; color:var(--text-light); margin-bottom:0.5rem;">Comments</div>
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.5rem;" id="comments-container-<?= $t['id'] ?>"> <div style="display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.5rem;" id="comments-container-<?= $t['id'] ?>">
@@ -731,6 +772,16 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php
$task_tags = !empty($t['tags']) ? array_filter(array_map('trim', explode(',', $t['tags']))) : [];
?>
<button type="button" onclick="showAddTagPopup(<?= $t['id'] ?>, '<?= htmlspecialchars(implode(',', $task_tags), ENT_QUOTES) ?>')" style="position:absolute; bottom:0.75rem; right:0.75rem; background:var(--primary); color:white; border:none; border-radius:50%; width:28px; height:28px; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:0.8rem; box-shadow:0 2px 4px rgba(0,0,0,0.2); z-index:10;" title="Manage Tags">
<i class="fas fa-tag"></i>
<?php if(!empty($task_tags)): ?>
<span style="position:absolute; top:-4px; right:-4px; background:#EF4444; color:white; border-radius:50%; width:14px; height:14px; font-size:0.6rem; display:flex; align-items:center; justify-content:center; font-weight:700;"><?= count($task_tags) ?></span>
<?php endif; ?>
</button>
</div> </div>
</div> </div>
@@ -802,10 +853,13 @@ function allowDrop(ev) {
function drag(ev, taskId) { function drag(ev, taskId) {
ev.dataTransfer.setData("text", 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) { function dragEnd(ev) {
ev.target.classList.remove('dragging'); let taskEl = ev.target.closest('.kanban-task');
if(taskEl) taskEl.classList.remove('dragging');
} }
function sortPendingTasks() { function sortPendingTasks() {
@@ -1230,8 +1284,124 @@ function confirmCustomDelete(form, msg) {
return false; 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 `<span style="font-size:0.7rem; padding:3px 10px; border-radius:12px; background:rgba(var(--primary-rgb),0.15); color:var(--primary); font-weight:600; display:inline-flex; align-items:center; gap:0.3rem; margin:0.2rem;">
<i class="fas fa-tag" style="font-size:0.6rem;"></i> ${tag}
<i class="fas fa-times" style="cursor:pointer; opacity:0.6; font-size:0.65rem;" onclick="removeTagFromPopup('${escaped}', ${taskId})" title="Remove tag"></i>
</span>`;
}).join('');
modal.innerHTML = `
<h3 style="margin-bottom:1rem; font-size:1rem; text-align:center;"><i class="fas fa-tags" style="color:var(--primary);"></i> Manage Tags</h3>
<div id="popup-tags-list" style="margin-bottom:1rem; min-height:30px; ${tags.length ? '' : 'display:none;'}">${tagsHtml}</div>
<div style="display:flex; gap:0.5rem; margin-bottom:1rem;">
<input type="text" id="popup-tag-input" placeholder="Add tags (comma separated)..." style="flex:1; border:1px solid var(--border); background:var(--bg); color:var(--text); border-radius:8px; padding:0.5rem; font-size:0.875rem; box-sizing:border-box;" onkeypress="if(event.key==='Enter'){event.preventDefault();addTagsFromPopup(${taskId});">
<button type="button" onclick="addTagsFromPopup(${taskId})" style="background:var(--primary); color:white; border:none; border-radius:8px; padding:0.5rem 1rem; font-size:0.8rem; cursor:pointer;"><i class="fas fa-plus"></i></button>
</div>
<div style="text-align:center;">
<button onclick="closeTagPopup()" style="background:var(--border); color:var(--text); border:none; border-radius:8px; padding:0.4rem 1.5rem; font-size:0.8rem; cursor:pointer;">Close</button>
</div>
`;
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) { function uploadFilesAjax(taskId, files) {
if(files.length === 0) return;
let container = document.getElementById('progress-container-' + taskId); let container = document.getElementById('progress-container-' + taskId);
let fill = document.getElementById('progress-fill-' + taskId); let fill = document.getElementById('progress-fill-' + taskId);