feat: enhance task tracking with comment metadata, tag support, improved audit logging, and flatpickr integration.
This commit is contained in:
56
index.php
56
index.php
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
date_default_timezone_set('Etc/GMT-7');
|
||||||
require_once 'db.php';
|
require_once 'db.php';
|
||||||
|
|
||||||
$action = (string)($_GET['action'] ?? 'dashboard');
|
$action = (string)($_GET['action'] ?? 'dashboard');
|
||||||
@@ -13,7 +14,7 @@ if ($action === 'api') {
|
|||||||
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_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 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 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 custom_assignee_name TEXT"); } catch(Exception $e) {}
|
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN custom_assignee_name TEXT"); } catch(Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completed_by INTEGER DEFAULT NULL"); } catch(Exception $e) {}
|
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completed_by INTEGER DEFAULT NULL"); } catch(Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN archived_by INTEGER DEFAULT NULL"); } catch(Exception $e) {}
|
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN archived_by INTEGER DEFAULT NULL"); } catch(Exception $e) {}
|
||||||
@@ -99,7 +100,16 @@ if ($action === 'api') {
|
|||||||
g.name as assignor_group_name,
|
g.name as assignor_group_name,
|
||||||
au.username as assignee_user_name,
|
au.username as assignee_user_name,
|
||||||
ag.name as assignee_group_name,
|
ag.name as assignee_group_name,
|
||||||
(SELECT COUNT(*) FROM task_attachments ta WHERE ta.task_id = t.id) AS attachments_count
|
(SELECT COUNT(*) FROM task_attachments ta WHERE ta.task_id = t.id) AS attachments_count,
|
||||||
|
(SELECT COUNT(*) FROM task_comments tc WHERE tc.task_id = t.id) AS comments_count,
|
||||||
|
(SELECT comment FROM task_comments tc WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_comment,
|
||||||
|
(SELECT created_at FROM task_comments tc WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_comment_date,
|
||||||
|
(SELECT cu.username FROM task_comments tc JOIN users cu ON tc.created_by = cu.id WHERE tc.task_id = t.id ORDER BY tc.created_at DESC LIMIT 1) AS last_commenter,
|
||||||
|
t.status_updated_at,
|
||||||
|
t.completed_by,
|
||||||
|
t.archived_by,
|
||||||
|
t.tags,
|
||||||
|
t.title
|
||||||
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
|
||||||
@@ -142,15 +152,35 @@ if ($action === 'api') {
|
|||||||
$desc = (string)($row['description'] ?? '');
|
$desc = (string)($row['description'] ?? '');
|
||||||
$desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? '');
|
$desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? '');
|
||||||
|
|
||||||
|
$tags = !empty($row['tags']) ? array_filter(array_map('trim', explode(',', $row['tags']))) : [];
|
||||||
|
|
||||||
|
// Logic for last_modified_by
|
||||||
|
// If it's archived, 'Archived by' user; if completed, 'Completed by' user; otherwise latest commenter or creator.
|
||||||
|
$last_mod_by = 'System';
|
||||||
|
if ($row['status'] === 'archived' && $row['archived_by']) {
|
||||||
|
$last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['archived_by']])->fetchColumn() ?: 'System';
|
||||||
|
} elseif ($row['status'] === 'completed' && $row['completed_by']) {
|
||||||
|
$last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['completed_by']])->fetchColumn() ?: 'System';
|
||||||
|
} else {
|
||||||
|
$last_mod_by = $row['last_commenter'] ?: $row['assignor_user_name'];
|
||||||
|
}
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'task_id' => (string)$row['id'],
|
'task_id' => (string)$row['id'],
|
||||||
'task_created_date' => $format_dt($row['created_at'] ?? null),
|
'task_name' => (string)$row['title'],
|
||||||
|
'task_created_date' => $format_dt($row['created_at'] ? $row['created_at'] . ' UTC' : null),
|
||||||
'from' => $from,
|
'from' => $from,
|
||||||
'to' => $to,
|
'to' => $to,
|
||||||
'task_description' => $desc,
|
'task_description' => $desc,
|
||||||
'due_date' => $format_dt($row['due_date'] ?? null),
|
'due_date' => $format_dt($row['due_date'] ?? null),
|
||||||
'current_status' => $format_status($row['status'] ?? null),
|
'current_status' => $format_status($row['status'] ?? null),
|
||||||
'attachments_count' => (int)($row['attachments_count'] ?? 0),
|
'attachments_count' => (int)($row['attachments_count'] ?? 0),
|
||||||
|
'tags_count' => count($tags),
|
||||||
|
'comments_count' => (int)($row['comments_count'] ?? 0),
|
||||||
|
'last_modified_by' => $last_mod_by,
|
||||||
|
'last_modified_date' => $format_dt(($row['status_updated_at'] ?: $row['created_at']) . ' UTC'),
|
||||||
|
'last_comment' => $row['last_comment'] ?: null,
|
||||||
|
'last_comment_date' => $format_dt($row['last_comment_date'] ? $row['last_comment_date'] . ' UTC' : null),
|
||||||
];
|
];
|
||||||
|
|
||||||
echo json_encode($payload);
|
echo json_encode($payload);
|
||||||
@@ -171,15 +201,33 @@ if ($action === 'api') {
|
|||||||
$desc = (string)($row['description'] ?? '');
|
$desc = (string)($row['description'] ?? '');
|
||||||
$desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? '');
|
$desc = trim($desc) !== '' ? $desc : (string)($row['title'] ?? '');
|
||||||
|
|
||||||
|
$tags = !empty($row['tags']) ? array_filter(array_map('trim', explode(',', $row['tags']))) : [];
|
||||||
|
|
||||||
|
$last_mod_by = 'System';
|
||||||
|
if ($row['status'] === 'archived' && $row['archived_by']) {
|
||||||
|
$last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['archived_by']])->fetchColumn() ?: 'System';
|
||||||
|
} elseif ($row['status'] === 'completed' && $row['completed_by']) {
|
||||||
|
$last_mod_by = query("SELECT username FROM users WHERE id=?", [$row['completed_by']])->fetchColumn() ?: 'System';
|
||||||
|
} else {
|
||||||
|
$last_mod_by = $row['last_commenter'] ?: $row['assignor_user_name'];
|
||||||
|
}
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'task_id' => (string)$row['id'],
|
'task_id' => (string)$row['id'],
|
||||||
'task_created_date' => $format_dt($row['created_at'] ?? null),
|
'task_name' => (string)$row['title'],
|
||||||
|
'task_created_date' => $format_dt($row['created_at'] ? $row['created_at'] . ' UTC' : null),
|
||||||
'from' => $from,
|
'from' => $from,
|
||||||
'to' => $to,
|
'to' => $to,
|
||||||
'task_description' => $desc,
|
'task_description' => $desc,
|
||||||
'due_date' => $format_dt($row['due_date'] ?? null),
|
'due_date' => $format_dt($row['due_date'] ?? null),
|
||||||
'current_status' => $format_status($row['status'] ?? null),
|
'current_status' => $format_status($row['status'] ?? null),
|
||||||
'attachments_count' => (int)($row['attachments_count'] ?? 0),
|
'attachments_count' => (int)($row['attachments_count'] ?? 0),
|
||||||
|
'tags_count' => count($tags),
|
||||||
|
'comments_count' => (int)($row['comments_count'] ?? 0),
|
||||||
|
'last_modified_by' => $last_mod_by,
|
||||||
|
'last_modified_date' => $format_dt(($row['status_updated_at'] ?: $row['created_at']) . ' UTC'),
|
||||||
|
'last_comment' => $row['last_comment'] ?: null,
|
||||||
|
'last_comment_date' => $format_dt($row['last_comment_date'] ? $row['last_comment_date'] . ' UTC' : null),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,24 @@
|
|||||||
<link rel="stylesheet" href="public/css/style.css">
|
<link rel="stylesheet" href="public/css/style.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
<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 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>
|
<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{ 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); }
|
.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>
|
</style>
|
||||||
<?php
|
<?php
|
||||||
|
date_default_timezone_set('Etc/GMT-7');
|
||||||
$all_app_tags = [];
|
$all_app_tags = [];
|
||||||
try {
|
try {
|
||||||
$rows = $pdo->query("SELECT label_name as tag FROM notes WHERE label_name IS NOT NULL AND label_name != ''")->fetchAll();
|
$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_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 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 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("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 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 {
|
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' => []];
|
$kanban = ['pending' => [], 'in_progress' => [], 'completed' => [], 'archived' => []];
|
||||||
foreach($tasks as $t) {
|
foreach($tasks as $t) {
|
||||||
if(isset($kanban[$t['status']])) $kanban[$t['status']][] = $t;
|
if(isset($kanban[$t['status']])) $kanban[$t['status']][] = $t;
|
||||||
@@ -246,7 +266,7 @@ foreach($tasks as $t) {
|
|||||||
usort($kanban['pending'], function($a, $b) {
|
usort($kanban['pending'], function($a, $b) {
|
||||||
$da = $a['due_date'] ? strtotime($a['due_date']) : PHP_INT_MAX;
|
$da = $a['due_date'] ? strtotime($a['due_date']) : PHP_INT_MAX;
|
||||||
$db = $b['due_date'] ? strtotime($b['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;
|
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;
|
-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); 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; }
|
.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); }
|
||||||
@@ -445,11 +465,11 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="position:relative; width:30px; height:30px;" title="Due Date">
|
<div style="position:relative;" title="Due Date & Time">
|
||||||
<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 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>
|
<i class="fas fa-calendar-alt" style="font-size:0.85rem;"></i>
|
||||||
</div>
|
</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>
|
</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;">
|
<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>
|
||||||
<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 ?>" 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 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;">
|
||||||
@@ -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;">
|
<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 = '';
|
// Determine assignor info
|
||||||
if($t['assignee_type'] == 'custom') $to_name = htmlspecialchars((string)$t['custom_assignee_name']);
|
$from_name = '';
|
||||||
elseif($t['assignee_type'] == 'group') $to_name = htmlspecialchars($t['assignee_group_name']);
|
if($t['assignor_type'] == 'group') $from_name = $t['assignor_group_name'];
|
||||||
else $to_name = htmlspecialchars($t['assignee_user_name'] ?? '');
|
else $from_name = $t['assignor_user_name'] ?? 'System';
|
||||||
|
$from_pic = ($t['assignor_type'] == 'user') ? ($user_pics[$from_name] ?? '') : '';
|
||||||
// Get assignee profile pic for user assignments
|
$from_initial = strtoupper(mb_substr($from_name, 0, 1));
|
||||||
$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;
|
|
||||||
$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];
|
$from_bg = $bg_colors[crc32($from_name) % 8];
|
||||||
|
|
||||||
echo '<div class="avatar-collapsed-only" style="display:flex; align-items:center;" title="To: ' . $to_name . '">';
|
$to_names_raw = '';
|
||||||
if($to_profile_pic && file_exists($to_profile_pic)) {
|
if($t['assignee_type'] == 'custom') $to_names_raw = (string)$t['custom_assignee_name'];
|
||||||
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;">';
|
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 {
|
} 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 '</div>';
|
||||||
|
|
||||||
echo '<span class="badge-role role-to expanded-only" style="padding:2px 8px; align-items:center; gap:0.4rem;">';
|
// Expanded View Badges
|
||||||
if($to_profile_pic && file_exists($to_profile_pic)) {
|
echo '<div class="expanded-only" style="display:flex; flex-wrap:wrap; gap:0.4rem;">';
|
||||||
echo '<img src="'.htmlspecialchars($to_profile_pic).'" style="width:18px; height:18px; border-radius:50%; object-fit:cover;">';
|
foreach($to_names as $name) {
|
||||||
} else {
|
$initial = strtoupper(mb_substr($name, 0, 1));
|
||||||
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>';
|
$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 '</div>';
|
||||||
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;
|
$tag_count = !empty($t['tags']) ? count(array_filter(array_map('trim', explode(',', $t['tags'])))) : 0;
|
||||||
if($tag_count > 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']]);
|
$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']) {
|
if($t['due_date']) {
|
||||||
$days_diff = (int)round((strtotime($t['due_date']) - strtotime(date('Y-m-d'))) / 86400);
|
$days_diff = (int)round((strtotime($t['due_date']) - strtotime(date('Y-m-d'))) / 86400);
|
||||||
if($days_diff < 0) {
|
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) {
|
} 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>';
|
echo '<span class="badge" style="background:#F59E0B; color:white; padding:2px 8px; border-radius:12px; font-weight:600;">Due Today</span>';
|
||||||
} else {
|
} 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>
|
<div class="kanban-task-desc"><?= nl2br(htmlspecialchars((string)$t['description'])) ?></div>
|
||||||
<?php endif; ?>
|
<?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 style="font-size:0.72rem; 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 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']): ?>
|
<?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; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</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') ? ($user_pics[$from_name] ?? '') : '';
|
||||||
$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>
|
||||||
@@ -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) ?>
|
<?= 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; flex-wrap:wrap; justify-content:flex-end; max-width:200px;">
|
||||||
<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, $to_profile_pic_expanded) ?>
|
<?php
|
||||||
|
foreach($to_names as $name) {
|
||||||
|
$p = $user_pics[$name] ?? '';
|
||||||
|
echo get_avatar_html_local($name, $p);
|
||||||
|
}
|
||||||
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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; ?>
|
<?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'] ?>">
|
||||||
<?php if(!empty($comments[$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;">
|
<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="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;">
|
<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>
|
<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'])): ?>
|
<?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>
|
<i class="fas fa-trash" style="cursor:pointer; color:#EF4444;" onclick="deleteComment(<?= $cm['id'] ?>)"></i>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -715,8 +756,17 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
|||||||
</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="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
|
<div style="position:absolute; bottom:-0.25rem; right:-0.25rem; display:flex; align-items:center; gap:0.5rem; padding:0.5rem;">
|
||||||
<div style="font-size:0.75rem; font-weight:600; color:var(--text-light);">Attachments</div>
|
<!-- 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;">
|
<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>
|
<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)">
|
<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>
|
</form>
|
||||||
<?php endif; ?>
|
<?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>
|
<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>
|
</div>
|
||||||
@@ -773,15 +829,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
|||||||
<?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>
|
||||||
@@ -1126,11 +1174,22 @@ function handleDueDateChange(val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearDueDate() {
|
function clearDueDate() {
|
||||||
let input = document.querySelector('input[name="due_date"]');
|
duePicker.clear();
|
||||||
input.value = '';
|
|
||||||
handleDueDateChange('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = [];
|
let selectedAssignees = [];
|
||||||
|
|
||||||
function handleToSelection(val) {
|
function handleToSelection(val) {
|
||||||
|
|||||||
Reference in New Issue
Block a user