diff --git a/index.php b/index.php index 5a444f3..c432e94 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,6 @@ 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 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 archived_by INTEGER DEFAULT NULL"); } catch(Exception $e) {} @@ -99,7 +100,16 @@ if ($action === 'api') { g.name as assignor_group_name, au.username as assignee_user_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 LEFT JOIN users u ON t.assignor_id = u.id LEFT JOIN groups g ON t.assignor_group_id = g.id @@ -142,15 +152,35 @@ if ($action === 'api') { $desc = (string)($row['description'] ?? ''); $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 = [ '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, 'to' => $to, 'task_description' => $desc, 'due_date' => $format_dt($row['due_date'] ?? null), 'current_status' => $format_status($row['status'] ?? null), '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); @@ -171,15 +201,33 @@ if ($action === 'api') { $desc = (string)($row['description'] ?? ''); $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[] = [ '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, 'to' => $to, 'task_description' => $desc, 'due_date' => $format_dt($row['due_date'] ?? null), 'current_status' => $format_status($row['status'] ?? null), '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), ]; } diff --git a/views/layout_top.php b/views/layout_top.php index 2f7adfe..5b31d20 100644 --- a/views/layout_top.php +++ b/views/layout_top.php @@ -9,12 +9,24 @@ + + + query("SELECT label_name as tag FROM notes WHERE label_name IS NOT NULL AND label_name != ''")->fetchAll(); diff --git a/views/tasks.php b/views/tasks.php index db5677a..8aa9c49 100644 --- a/views/tasks.php +++ b/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 -
-
+
+
- +
-
+
@@ -528,40 +548,80 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
'; - if($to_profile_pic && file_exists($to_profile_pic)) { - echo ''; + $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 '
'; + + // From Avatar + if ($from_pic && file_exists($from_pic)) { + echo ''; } else { - echo '
'.$to_initial.'
'; + echo '
'.$from_initial.'
'; + } + + // Arrow style separator + echo ''; + + // To Avatar(s) stack + echo '
'; + 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 ''; + } else { + echo '
'.$initial.'
'; + } } echo '
'; + echo '
'; - echo ''; - if($to_profile_pic && file_exists($to_profile_pic)) { - echo ''; - } else { - echo '
'.$to_initial.'
'; + // Expanded View Badges + echo '
'; + 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 ''; + if ($pic && file_exists($pic)) { + echo ''; + } else { + echo '
'.$initial.'
'; + } + echo ''.htmlspecialchars($name).''; + echo '
'; } - echo ''.$to_name.''; - echo ''; + echo '
'; echo '
'; $tag_count = !empty($t['tags']) ? count(array_filter(array_map('trim', explode(',', $t['tags'])))) : 0; if($tag_count > 0) { - echo ' '.$tag_count.''; + echo ' '.$tag_count.''; } $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 'Overdue by '.abs($days_diff).'d'; + echo 'Overdue '.abs($days_diff).'d ago'; } elseif($days_diff == 0) { echo 'Due Today'; } else { @@ -595,10 +655,10 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
-
-
Created:
+
+
-
Due Date:
+
@@ -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] ?? '') : ''; ?>
Assignments: @@ -635,9 +693,14 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
-
+
To - +
@@ -659,42 +722,20 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
- -
-
-
Tags
- - - -
-
- - - - - - - - - -
-
-
Comments
- +
- +
- +   @@ -715,8 +756,17 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
-
-
Attachments
+
+ + + + +
@@ -773,15 +829,7 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
- - +
@@ -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) {