feat: enhance task tracking with comment metadata, tag support, improved audit logging, and flatpickr integration.
This commit is contained in:
@@ -9,12 +9,24 @@
|
||||
<link rel="stylesheet" href="public/css/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<style>
|
||||
.tags-look .tagify__dropdown__item{ display: inline-block; border-radius: 3px; padding: .3em .5em; border: 1px solid var(--border); background: var(--surface); margin: .2em; font-size: .85em; transition: 0s; color: var(--text); }
|
||||
.tags-look .tagify__dropdown__item--active{ color: black; }
|
||||
.tags-look .tagify__dropdown__item--active{ color: white; background: var(--primary); border-color: var(--primary); }
|
||||
.tags-look .tagify__dropdown__item:hover{ background: var(--primary); color: white; border-color: var(--primary); }
|
||||
|
||||
/* Premium Dark Mode Tagify Sync */
|
||||
.tagify { --tag-bg: rgba(var(--primary-rgb), 0.2); --tag-text-color: var(--text); --tag-hover: rgba(var(--primary-rgb), 0.3); --placeholder-color: var(--text-light); }
|
||||
body.dark-mode .tagify { --tag-bg: #2d3748; --tag-text-color: #e2e8f0; --tag-hover: #4a5568; }
|
||||
.tagify__tag > div { background: var(--tag-bg); color: var(--tag-text-color); border-radius: 6px; }
|
||||
.tagify__tag:hover:not([readonly]) > div::before { background: var(--tag-hover); }
|
||||
.tagify__dropdown { background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px); border: 1px solid var(--border); border-radius: 8px; }
|
||||
.tagify__dropdown__wrapper { background: transparent; border: none; }
|
||||
</style>
|
||||
<?php
|
||||
date_default_timezone_set('Etc/GMT-7');
|
||||
$all_app_tags = [];
|
||||
try {
|
||||
$rows = $pdo->query("SELECT label_name as tag FROM notes WHERE label_name IS NOT NULL AND label_name != ''")->fetchAll();
|
||||
|
||||
221
views/tasks.php
221
views/tasks.php
@@ -3,7 +3,7 @@
|
||||
try { $pdo->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("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 {
|
||||
@@ -237,6 +237,26 @@ if($t_ids) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -246,7 +266,7 @@ foreach($tasks as $t) {
|
||||
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']);
|
||||
if($da == $db) return strtotime($b['created_at'] . ' UTC') - strtotime($a['created_at'] . ' UTC');
|
||||
return $da - $db;
|
||||
});
|
||||
?>
|
||||
@@ -306,7 +326,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); position: relative;
|
||||
display: none; padding-top: 0.75rem; margin-top: 0.5rem; border-top: 1px dashed var(--border); position: relative; padding-bottom: 2.5rem;
|
||||
}
|
||||
.kanban-task.expanded .kanban-task-body { display: block; }
|
||||
.chevron { transition: transform 0.2s; font-size: 0.8rem; color: var(--text-light); }
|
||||
@@ -445,11 +465,11 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:relative; width:30px; height:30px;" title="Due Date">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:100%; height:100%; border-radius:50%; background:var(--surface); border:1px solid var(--border); color:var(--text-light); transition:background 0.2s; position:absolute; top:0; left:0; z-index:1; pointer-events:none;">
|
||||
<div style="position:relative;" title="Due Date & Time">
|
||||
<div id="due-icon-btn" style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:30px; height:30px; border-radius:50%; background:var(--surface); border:1px solid var(--border); color:var(--text-light); transition:all 0.2s;" onclick="duePicker.open();">
|
||||
<i class="fas fa-calendar-alt" style="font-size:0.85rem;"></i>
|
||||
</div>
|
||||
<input type="date" name="due_date" style="position:absolute; top:0; left:0; width:100%; height:100%; opacity:0; cursor:pointer; z-index:2;" title="Set Due Date" onchange="handleDueDateChange(this.value)">
|
||||
<input type="text" name="due_date" id="due-picker-input" style="visibility:hidden; position:absolute; width:1px; height:1px;">
|
||||
</div>
|
||||
|
||||
<label title="Attach Files" id="attach-icon-btn" style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:30px; height:30px; border-radius:50%; background:var(--surface); border:1px solid var(--border); color:var(--text-light); transition:background 0.2s;">
|
||||
@@ -501,7 +521,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
</div>
|
||||
<div class="kanban-tasks" id="col-<?= $col_id ?>">
|
||||
<?php foreach($kanban[$col_id] as $t): ?>
|
||||
<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 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'] . ' UTC') ?>">
|
||||
|
||||
<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;">
|
||||
@@ -528,40 +548,80 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; margin-top:0.5rem;">
|
||||
<?php
|
||||
$to_name = '';
|
||||
if($t['assignee_type'] == 'custom') $to_name = htmlspecialchars((string)$t['custom_assignee_name']);
|
||||
elseif($t['assignee_type'] == 'group') $to_name = htmlspecialchars($t['assignee_group_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));
|
||||
$col_idx = crc32($to_name) % 8;
|
||||
// Determine assignor info
|
||||
$from_name = '';
|
||||
if($t['assignor_type'] == 'group') $from_name = $t['assignor_group_name'];
|
||||
else $from_name = $t['assignor_user_name'] ?? 'System';
|
||||
$from_pic = ($t['assignor_type'] == 'user') ? ($user_pics[$from_name] ?? '') : '';
|
||||
$from_initial = strtoupper(mb_substr($from_name, 0, 1));
|
||||
$bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
|
||||
$av_bg = $bg_colors[$col_idx];
|
||||
$from_bg = $bg_colors[crc32($from_name) % 8];
|
||||
|
||||
echo '<div class="avatar-collapsed-only" style="display:flex; align-items:center;" title="To: ' . $to_name . '">';
|
||||
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;">';
|
||||
$to_names_raw = '';
|
||||
if($t['assignee_type'] == 'custom') $to_names_raw = (string)$t['custom_assignee_name'];
|
||||
elseif($t['assignee_type'] == 'group') $to_names_raw = htmlspecialchars($t['assignee_group_name']);
|
||||
else $to_names_raw = htmlspecialchars($t['assignee_user_name'] ?? '');
|
||||
|
||||
$to_names = array_filter(array_map('trim', explode(',', $to_names_raw)));
|
||||
|
||||
$to_names_str = implode(', ', $to_names);
|
||||
$assignment_title = "Task Assigned from {$from_name} to {$to_names_str}";
|
||||
|
||||
// Collapsed View: {assignor} => {assignee}
|
||||
echo '<div class="avatar-collapsed-only" style="display:flex; align-items:center; gap:0.4rem;" title="'.htmlspecialchars($assignment_title).'">';
|
||||
|
||||
// From Avatar
|
||||
if ($from_pic && file_exists($from_pic)) {
|
||||
echo '<img src="'.htmlspecialchars($from_pic).'" style="width:24px; height:24px; border-radius:50%; object-fit:cover; border:2px solid var(--surface);">';
|
||||
} 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 style="width:24px; height:24px; border-radius:50%; background:'.$from_bg.'; color:white; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:0.75rem; border:2px solid var(--surface);">'.$from_initial.'</div>';
|
||||
}
|
||||
|
||||
// Arrow style separator
|
||||
echo '<i class="fas fa-chevron-right" style="font-size:0.6rem; color:var(--text-light); opacity:0.6;"></i>';
|
||||
|
||||
// To Avatar(s) stack
|
||||
echo '<div style="display:flex; align-items:center;">';
|
||||
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 '<img src="'.htmlspecialchars($pic).'" style="width:24px; height:24px; border-radius:50%; object-fit:cover; border:2px solid var(--surface); margin-left:'.$margin.'; z-index:'.(10 - $idx).';">';
|
||||
} 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-left:'.$margin.'; z-index:'.(10 - $idx).';">'.$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;">';
|
||||
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>';
|
||||
// Expanded View Badges
|
||||
echo '<div class="expanded-only" style="display:flex; flex-wrap:wrap; gap:0.4rem;">';
|
||||
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 '<span class="badge-role role-to" style="padding:2px 8px; align-items:center; gap:0.4rem; display:inline-flex;">';
|
||||
if ($pic && file_exists($pic)) {
|
||||
echo '<img src="'.htmlspecialchars($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;">'.$initial.'</div>';
|
||||
}
|
||||
echo '<span>'.htmlspecialchars($name).'</span>';
|
||||
echo '</span>';
|
||||
}
|
||||
echo '<span>'.$to_name.'</span>';
|
||||
echo '</span>';
|
||||
echo '</div>';
|
||||
|
||||
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>';
|
||||
echo '<span style="color:var(--text-light); 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']]);
|
||||
@@ -578,7 +638,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
if($t['due_date']) {
|
||||
$days_diff = (int)round((strtotime($t['due_date']) - strtotime(date('Y-m-d'))) / 86400);
|
||||
if($days_diff < 0) {
|
||||
echo '<span class="badge" style="background:#EF4444; color:white; padding:2px 8px; border-radius:12px; font-weight:600;">Overdue by '.abs($days_diff).'d</span>';
|
||||
echo '<span class="badge" style="background:#EF4444; color:white; padding:2px 8px; border-radius:12px; font-weight:600;">Overdue '.abs($days_diff).'d ago</span>';
|
||||
} elseif($days_diff == 0) {
|
||||
echo '<span class="badge" style="background:#F59E0B; color:white; padding:2px 8px; border-radius:12px; font-weight:600;">Due Today</span>';
|
||||
} else {
|
||||
@@ -595,10 +655,10 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<div class="kanban-task-desc"><?= nl2br(htmlspecialchars((string)$t['description'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="font-size:0.75rem; color:var(--text-light); margin-bottom:0.75rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div><i class="fas fa-calendar-plus" style="opacity:0.7; margin-right:4px;"></i> Created: <?= date('M j, Y', strtotime($t['created_at'])) ?></div>
|
||||
<div style="font-size:0.72rem; color:var(--text-light); margin-bottom:0.75rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div title="Created At"><i class="fas fa-calendar-plus" style="opacity:0.7; margin-right:4px;"></i> <?= date('M j, Y H:i', strtotime($t['created_at'] . ' UTC')) ?></div>
|
||||
<?php if($t['due_date']): ?>
|
||||
<div><i class="fas fa-clock" style="color:#EF4444; margin-right:4px;"></i> Due Date: <strong style="color:#EF4444;"><?= date('M j, Y', strtotime($t['due_date'])) ?></strong></div>
|
||||
<div title="Due Date"><i class="fas fa-clock" style="color:#EF4444; margin-right:4px;"></i> <strong style="color:#EF4444;"><?= date('M j, Y H:i', strtotime($t['due_date'])) ?></strong></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -623,9 +683,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
}
|
||||
}
|
||||
|
||||
// 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'] ?? '') : '';
|
||||
$from_profile_pic = ($t['assignor_type'] == 'user') ? ($user_pics[$from_name] ?? '') : '';
|
||||
?>
|
||||
<div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
<span style="color:var(--text-light);">Assignments:</span>
|
||||
@@ -635,9 +693,14 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<?= get_avatar_html_local($from_name, $from_profile_pic) ?>
|
||||
</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; flex-wrap:wrap; justify-content:flex-end; max-width:200px;">
|
||||
<span style="font-size:0.65rem; color:var(--text-light);">To</span>
|
||||
<?= get_avatar_html_local($to_name, $to_profile_pic_expanded) ?>
|
||||
<?php
|
||||
foreach($to_names as $name) {
|
||||
$p = $user_pics[$name] ?? '';
|
||||
echo get_avatar_html_local($name, $p);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -659,42 +722,20 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<?php endif; ?>
|
||||
</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="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'] ?>">
|
||||
<?php if(!empty($comments[$t['id']])): ?>
|
||||
<?php foreach($comments[$t['id']] as $cm): ?>
|
||||
<?php foreach($comments[$t['id']] as $cm):
|
||||
$commenter_name = $cm['created_by_username'] ?? 'User';
|
||||
$commenter_pic = $user_pics[$commenter_name] ?? '';
|
||||
?>
|
||||
<div style="display:flex; gap:0.4rem; align-items:flex-start;">
|
||||
<?= get_avatar_html_local($cm['created_by_username'] ?? 'User') ?>
|
||||
<?= get_avatar_html_local($commenter_name, $commenter_pic) ?>
|
||||
<div style="background:var(--bg); border:1px solid var(--border); border-radius:12px; border-top-left-radius:0; padding:0.4rem 0.6rem; font-size:0.75rem; color:var(--text); flex:1;">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:0.2rem;">
|
||||
<strong style="font-size:0.65rem; color:var(--primary);"><?= htmlspecialchars($cm['created_by_username'] ?? 'User') ?></strong>
|
||||
<span style="font-size:0.6rem; color:var(--text-light);"><?= date('M j, H:i', strtotime($cm['created_at'])) ?>
|
||||
<span style="font-size:0.6rem; color:var(--text-light);"><?= date('M j, Y H:i', strtotime($cm['created_at'] . ' UTC')) ?>
|
||||
<?php if($cm['created_by'] == $user_id || !empty($_SESSION['is_admin'])): ?>
|
||||
<i class="fas fa-trash" style="cursor:pointer; color:#EF4444;" onclick="deleteComment(<?= $cm['id'] ?>)"></i>
|
||||
<?php endif; ?>
|
||||
@@ -715,8 +756,17 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
</div>
|
||||
|
||||
<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);">Attachments</div>
|
||||
<div style="position:absolute; bottom:-0.25rem; right:-0.25rem; display:flex; align-items:center; gap:0.5rem; padding:0.5rem;">
|
||||
<!-- Tag icon styled like attachment icon -->
|
||||
<?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="cursor:pointer; display:flex; align-items:center; justify-content:center; width:22px; height:22px; border-radius:4px; background:var(--border); color:var(--text); border:none; transition:background 0.2s; position:relative;" title="Manage Tags">
|
||||
<i class="fas fa-tag" style="font-size:0.6rem;"></i>
|
||||
<?php if(!empty($task_tags)): ?>
|
||||
<span style="position:absolute; top:-4px; right:-4px; background:#EF4444; color:white; border-radius:50%; width:12px; height:12px; font-size:0.5rem; display:flex; align-items:center; justify-content:center; font-weight:700; border:1px solid var(--surface);"><?= count($task_tags) ?></span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
|
||||
<!-- Attachment icon -->
|
||||
<label title="Attach files" style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:22px; height:22px; border-radius:4px; background:var(--border); color:var(--text); transition:background 0.2s;">
|
||||
<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>
|
||||
<input type="file" multiple style="display:none;" onchange="uploadFilesAjax(<?= $t['id'] ?>, this.files)">
|
||||
@@ -764,7 +814,13 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div title="Uploaded by <?= htmlspecialchars((string)$att['owner_name']) ?>" style="position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:var(--primary); color:white; display:flex; align-items:center; justify-content:center; font-size:0.5rem; font-weight:700; z-index:10; box-shadow:0 1px 2px rgba(0,0,0,0.3);"><?= $initial ?></div>
|
||||
<?php
|
||||
$att_owner_pic = $user_pics[$att['owner_name']] ?? '';
|
||||
if($att_owner_pic && file_exists($att_owner_pic)): ?>
|
||||
<img src="<?= htmlspecialchars($att_owner_pic) ?>" title="Uploaded by <?= htmlspecialchars((string)$att['owner_name']) ?>" style="position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; object-fit:cover; z-index:10; box-shadow:0 1px 2px rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.2);">
|
||||
<?php else: ?>
|
||||
<div title="Uploaded by <?= htmlspecialchars((string)$att['owner_name']) ?>" style="position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:var(--primary); color:white; display:flex; align-items:center; justify-content:center; font-size:0.5rem; font-weight:700; z-index:10; box-shadow:0 1px 2px rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.2);"><?= $initial ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?= htmlspecialchars((string)$att['file_path']) ?>" target="_blank" style="background:rgba(0,0,0,0.6); color:white; font-size:0.5rem; padding:2px 3px; text-decoration:none; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; position:absolute; bottom:0; left:0; right:0; text-align:center;" title="<?= htmlspecialchars((string)$att['file_name']) ?>"><?= htmlspecialchars((string)$att['file_name']) ?></a>
|
||||
</div>
|
||||
@@ -773,15 +829,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
@@ -1126,11 +1174,22 @@ function handleDueDateChange(val) {
|
||||
}
|
||||
|
||||
function clearDueDate() {
|
||||
let input = document.querySelector('input[name="due_date"]');
|
||||
input.value = '';
|
||||
handleDueDateChange('');
|
||||
duePicker.clear();
|
||||
}
|
||||
|
||||
let duePicker;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
duePicker = flatpickr("#due-picker-input", {
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d H:i",
|
||||
altInput: false,
|
||||
disableMobile: "true",
|
||||
onChange: function(selectedDates, dateStr) {
|
||||
handleDueDateChange(dateStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let selectedAssignees = [];
|
||||
|
||||
function handleToSelection(val) {
|
||||
|
||||
Reference in New Issue
Block a user