feat: replace subtasks with task commenting system and update task view UI
This commit is contained in:
26
index.php
26
index.php
@@ -18,7 +18,7 @@ if ($action === 'api') {
|
||||
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("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_subtasks (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, title TEXT NOT NULL, is_completed INTEGER DEFAULT 0, created_by INTEGER, completed_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("CREATE TABLE IF NOT EXISTS task_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, comment TEXT NOT NULL, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {}
|
||||
|
||||
$format_dt = function($value) {
|
||||
$value = is_string($value) ? trim($value) : $value;
|
||||
@@ -62,27 +62,27 @@ if ($action === 'api') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if($api_action === 'add_subtask') {
|
||||
if($api_action === 'add_comment') {
|
||||
if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized']));
|
||||
$task_id = $_POST['task_id'] ?? null;
|
||||
$title = $_POST['title'] ?? null;
|
||||
$comment = $_POST['comment'] ?? null;
|
||||
$user_id = $_SESSION['user_id'];
|
||||
if($task_id && $title) {
|
||||
$stmt = $pdo->prepare("INSERT INTO task_subtasks (task_id, title, created_by) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$task_id, $title, $user_id]);
|
||||
if($task_id && $comment) {
|
||||
$stmt = $pdo->prepare("INSERT INTO task_comments (task_id, comment, created_by) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$task_id, $comment, $user_id]);
|
||||
$t = query("SELECT title FROM tasks WHERE id=?", [$task_id])->fetch();
|
||||
if($t) notify_waha('Commented on', 'Task', $t['title']);
|
||||
}
|
||||
echo json_encode(['status'=>'ok']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if($api_action === 'toggle_subtask') {
|
||||
if($api_action === 'delete_comment') {
|
||||
if(!isset($_SESSION['user_id'])) die(json_encode(['status'=>'error','msg'=>'unauthorized']));
|
||||
$subtask_id = $_POST['subtask_id'] ?? null;
|
||||
$is_completed = (int)($_POST['is_completed'] ?? 0);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
if($subtask_id) {
|
||||
$stmt = $pdo->prepare("UPDATE task_subtasks SET is_completed = ?, completed_by = ? WHERE id = ?");
|
||||
$stmt->execute([$is_completed, $is_completed ? $user_id : null, $subtask_id]);
|
||||
$comment_id = $_POST['comment_id'] ?? null;
|
||||
if($comment_id) {
|
||||
$stmt = $pdo->prepare("DELETE FROM task_comments WHERE id = ?");
|
||||
$stmt->execute([$comment_id]);
|
||||
}
|
||||
echo json_encode(['status'=>'ok']);
|
||||
exit;
|
||||
|
||||
131
views/tasks.php
131
views/tasks.php
@@ -221,7 +221,7 @@ $tasks = $stmt->fetchAll();
|
||||
|
||||
$t_ids = array_column($tasks, 'id');
|
||||
$attachments = [];
|
||||
$subtasks = [];
|
||||
$comments = [];
|
||||
if($t_ids) {
|
||||
$in = str_repeat('?,', count($t_ids) - 1) . '?';
|
||||
$stmt = $pdo->prepare("SELECT ta.*, u.username as owner_name FROM task_attachments ta LEFT JOIN users u ON ta.owner_id = u.id WHERE ta.task_id IN ($in) ORDER BY ta.created_at ASC");
|
||||
@@ -230,10 +230,10 @@ if($t_ids) {
|
||||
$attachments[$row['task_id']][] = $row;
|
||||
}
|
||||
|
||||
$stmt2 = $pdo->prepare("SELECT ts.*, cbu.username as completed_by_username FROM task_subtasks ts LEFT JOIN users cbu ON ts.completed_by = cbu.id WHERE ts.task_id IN ($in) ORDER BY ts.created_at ASC");
|
||||
$stmt2 = $pdo->prepare("SELECT tc.*, cu.username as created_by_username FROM task_comments tc LEFT JOIN users cu ON tc.created_by = cu.id WHERE tc.task_id IN ($in) ORDER BY tc.created_at ASC");
|
||||
$stmt2->execute($t_ids);
|
||||
foreach($stmt2->fetchAll() as $row) {
|
||||
$subtasks[$row['task_id']][] = $row;
|
||||
$comments[$row['task_id']][] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,12 +560,10 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
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>';
|
||||
}
|
||||
|
||||
// We could show subtasks count too on collapsed state
|
||||
$sub_count = empty($subtasks[$t['id']]) ? 0 : count($subtasks[$t['id']]);
|
||||
$sub_done = 0;
|
||||
if($sub_count > 0) {
|
||||
foreach($subtasks[$t['id']] as $st) if($st['is_completed']) $sub_done++;
|
||||
echo '<span style="color:var(--text-light); font-weight:600; display:flex; align-items:center; gap:0.15rem;" title="'.$sub_done.'/'.$sub_count.' subtasks"><i class="fas fa-check-square" style="font-size:0.75rem;"></i> '.$sub_done.'/'.$sub_count.'</span>';
|
||||
// We could show comments count too on collapsed state
|
||||
$comment_count = empty($comments[$t['id']]) ? 0 : count($comments[$t['id']]);
|
||||
if($comment_count > 0) {
|
||||
echo '<span style="color:var(--text-light); font-weight:600; display:flex; align-items:center; gap:0.15rem;" title="'.$comment_count.' comment(s)"><i class="fas fa-comment-dots" style="font-size:0.75rem;"></i> '.$comment_count.'</span>';
|
||||
}
|
||||
|
||||
if($t['due_date']) {
|
||||
@@ -588,32 +586,44 @@ 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;">
|
||||
<div>📅 Created: <?= date('M j, Y', strtotime($t['created_at'])) ?></div>
|
||||
<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>
|
||||
<?php if($t['due_date']): ?>
|
||||
<div style="margin-top:2px;">⏰ Due Date: <strong style="color:#EF4444;"><?= date('M j, Y', strtotime($t['due_date'])) ?></strong></div>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:0.5rem;">
|
||||
<?php
|
||||
// Calculate FROM and TO names
|
||||
$from_name = '';
|
||||
if($t['assignor_type'] == 'group') $from_name = $t['assignor_group_name'];
|
||||
else $from_name = $t['assignor_user_name'] ?? 'System';
|
||||
|
||||
if (!function_exists('get_avatar_html_local')) {
|
||||
function get_avatar_html_local($name) {
|
||||
if (!$name) return '';
|
||||
$initial = strtoupper(mb_substr($name, 0, 1));
|
||||
$col_idx = crc32($name) % 8;
|
||||
$bg_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
|
||||
$av_bg = $bg_colors[$col_idx];
|
||||
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>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
<span style="color:var(--text-light);">From:</span>
|
||||
<span class="badge-role role-from">
|
||||
<?php
|
||||
if($t['assignor_type'] == 'group') echo '👥 '.htmlspecialchars($t['assignor_group_name']);
|
||||
else echo '👤 '.htmlspecialchars($t['assignor_user_name'] ?? '');
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
<span style="color:var(--text-light);">To:</span>
|
||||
<span class="badge-role role-to">
|
||||
<?php
|
||||
if($t['assignee_type'] == 'custom') echo '👤 '.htmlspecialchars((string)$t['custom_assignee_name']);
|
||||
elseif($t['assignee_type'] == 'group') echo '👥 '.htmlspecialchars($t['assignee_group_name']);
|
||||
else echo '👤 '.htmlspecialchars($t['assignee_user_name'] ?? '');
|
||||
?>
|
||||
</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.4rem;">
|
||||
<span style="font-size:0.65rem; color:var(--text-light);">From</span>
|
||||
<?= get_avatar_html_local($from_name) ?>
|
||||
</div>
|
||||
<div style="color:var(--border);">|</div>
|
||||
<div style="display:flex; align-items:center; gap:0.4rem;">
|
||||
<span style="font-size:0.65rem; color:var(--text-light);">To</span>
|
||||
<?= get_avatar_html_local($to_name) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if($t['status'] === 'completed'): ?>
|
||||
<div style="font-size:0.8rem; display:flex; justify-content:space-between; align-items:center;">
|
||||
@@ -633,38 +643,35 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if($t['status'] === 'in_progress' || !empty($subtasks[$t['id']])): ?>
|
||||
<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;">Subtasks / Progress</div>
|
||||
<div style="display:flex; flex-direction:column; gap:0.4rem; margin-bottom:0.5rem;" id="subtasks-container-<?= $t['id'] ?>">
|
||||
<?php if(!empty($subtasks[$t['id']])): ?>
|
||||
<?php foreach($subtasks[$t['id']] as $st): ?>
|
||||
<div style="display:flex; align-items:flex-start; gap:0.5rem; font-size:0.8rem;">
|
||||
<input type="checkbox" style="margin-top:0.2rem;" <?= $st['is_completed'] ? 'checked' : '' ?> <?= $t['status'] !== 'in_progress' ? 'disabled' : '' ?> onchange="toggleSubtask(<?= $st['id'] ?>, this.checked)">
|
||||
<div style="flex:1;">
|
||||
<span style="<?= $st['is_completed'] ? 'text-decoration:line-through; color:var(--text-light);' : 'color:var(--text);' ?>">
|
||||
<?= htmlspecialchars($st['title']) ?>
|
||||
</span>
|
||||
<?php if($st['is_completed']): ?>
|
||||
<div style="font-size:0.65rem; color:var(--text-light); margin-top:0.1rem;">
|
||||
Completed by <?= htmlspecialchars($st['completed_by_username'] ?? 'Unknown user') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<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): ?>
|
||||
<div style="display:flex; gap:0.4rem; align-items:flex-start;">
|
||||
<?= get_avatar_html_local($cm['created_by_username'] ?? 'User') ?>
|
||||
<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'])) ?>
|
||||
<?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; ?>
|
||||
</span>
|
||||
</div>
|
||||
<?= nl2br(htmlspecialchars($cm['comment'])) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.75rem; color:var(--text-light); font-style:italic;">No progress updates yet.</div>
|
||||
<div style="font-size:0.75rem; color:var(--text-light); font-style:italic;">No comments yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if($t['status'] === 'in_progress'): ?>
|
||||
<form method="POST" onsubmit="event.preventDefault(); addSubtask(<?= $t['id'] ?>, this);" style="display:flex; gap:0.25rem;">
|
||||
<input type="text" name="title" placeholder="Add progress or subtask..." required style="flex:1; border:1px solid var(--border); background:var(--bg); color:var(--text); border-radius:4px; padding:0.25rem 0.5rem; font-size:0.75rem; min-width:0;">
|
||||
<button type="submit" style="background:var(--primary); color:white; border:none; border-radius:4px; padding:0.25rem 0.5rem; font-size:0.75rem; cursor:pointer;" title="Add">Add</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="POST" onsubmit="event.preventDefault(); addComment(<?= $t['id'] ?>, this);" style="display:flex; gap:0.4rem;">
|
||||
<input type="text" name="comment" placeholder="Write a comment..." required style="flex:1; border:1px solid var(--border); background:var(--bg); color:var(--text); border-radius:20px; padding:0.4rem 0.75rem; font-size:0.75rem; min-width:0;">
|
||||
<button type="submit" style="background:var(--primary); color:white; border:none; border-radius:20px; padding:0.4rem 0.8rem; font-size:0.75rem; cursor:pointer;" title="Send"><i class="fas fa-paper-plane"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<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;">
|
||||
@@ -859,25 +866,25 @@ function drop(ev, newStatus) {
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
});
|
||||
}
|
||||
function addSubtask(taskId, form) {
|
||||
let title = form.title.value;
|
||||
if(!title) return;
|
||||
fetch('?action=api&api_action=add_subtask', {
|
||||
function addComment(taskId, form) {
|
||||
let comment = form.comment.value;
|
||||
if(!comment) return;
|
||||
fetch('?action=api&api_action=add_comment', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `task_id=${taskId}&title=${encodeURIComponent(title)}`
|
||||
body: `task_id=${taskId}&comment=${encodeURIComponent(comment)}`
|
||||
}).then(() => {
|
||||
form.reset();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
function toggleSubtask(subtaskId, isCompleted) {
|
||||
fetch('?action=api&api_action=toggle_subtask', {
|
||||
function deleteComment(commentId) {
|
||||
if(!confirm('Delete this comment?')) return;
|
||||
fetch('?action=api&api_action=delete_comment', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `subtask_id=${subtaskId}&is_completed=${isCompleted ? 1 : 0}`
|
||||
body: `comment_id=${commentId}`
|
||||
}).then(() => {
|
||||
// Quick visual toggle doesn't strictly need reload, but reloading ensures names are updated
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user