From fc045a0c4bb721de79538f294027969d26f247f2 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Fri, 4 Sep 2026 23:20:27 -0600 Subject: [PATCH] Match dashboard filter/sort behavior in offline task app The offline task app was showing every cached task including completed ones, with no sorting -- unlike the online dashboard, which hides completed tasks by default and sorts by due date. Ports the same filter tabs (All/Today/Upcoming/Overdue/Completed) and sort options (due date asc/desc, priority high/low) from tasks/views.py's DashboardView so offline mode isn't a degraded view. Co-Authored-By: Claude Sonnet 5 --- static/js/offline-tasks.js | 98 +++++++++++++++++++++++++++++++++--- templates/offline_tasks.html | 14 ++++++ templates/sw.js | 2 +- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/static/js/offline-tasks.js b/static/js/offline-tasks.js index 24b8f4e..f0aed8c 100644 --- a/static/js/offline-tasks.js +++ b/static/js/offline-tasks.js @@ -2,15 +2,73 @@ * KeepItGoing - Offline mini task app * Renders and edits tasks straight from IndexedDB while there's no network. * Reuses the .task-item/.task-checkbox/.priority-badge classes from - * templates/tasks/_task_item.html for visual consistency with the real app. + * templates/tasks/_task_item.html for visual consistency with the real app, + * and mirrors the dashboard's default filter/sort behavior (tasks/views.py + * DashboardView) so offline mode isn't a degraded, unsorted, unfiltered view. */ +const PRIORITY_ORDER = { urgent: 4, high: 3, medium: 2, low: 1 }; + +let currentFilter = 'all'; +let currentSort = 'due_date'; + +function todayLocalStr() { + const d = new Date(); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + function formatDueDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr + 'T00:00:00'); return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } +function applyFilter(tasks, filter) { + const today = todayLocalStr(); + if (filter === 'completed') { + return tasks.filter((t) => t.status === 'completed'); + } + // Every other filter excludes completed by default, matching the + // online dashboard's default behavior. + const notCompleted = tasks.filter((t) => t.status !== 'completed'); + switch (filter) { + case 'today': + return notCompleted.filter((t) => t.due_date === today); + case 'upcoming': + return notCompleted.filter((t) => t.due_date && t.due_date > today); + case 'overdue': + return notCompleted.filter((t) => t.due_date && t.due_date < today && t.status !== 'cancelled'); + default: + return notCompleted; + } +} + +function applySort(tasks, sort) { + const sorted = [...tasks]; + const dueOrDefault = (t) => t.due_date || '9999-99-99'; + const priorityOf = (t) => PRIORITY_ORDER[t.priority] || 0; + + switch (sort) { + case 'due_date_desc': + sorted.sort((a, b) => dueOrDefault(b).localeCompare(dueOrDefault(a)) || priorityOf(b) - priorityOf(a)); + break; + case 'priority': + sorted.sort((a, b) => priorityOf(b) - priorityOf(a) || dueOrDefault(a).localeCompare(dueOrDefault(b))); + break; + case 'priority_low': + sorted.sort((a, b) => priorityOf(a) - priorityOf(b) || dueOrDefault(a).localeCompare(dueOrDefault(b))); + break; + case 'due_date': + default: + sorted.sort((a, b) => dueOrDefault(a).localeCompare(dueOrDefault(b)) || priorityOf(b) - priorityOf(a)); + break; + } + return sorted; +} + function renderTaskItem(task) { const item = document.createElement('div'); item.className = `task-item priority-${task.priority}${task.status === 'completed' ? ' completed' : ''}`; @@ -98,17 +156,18 @@ async function renderOfflineTasks() { if (!listEl) return; const allTasks = await getAllTasks(); - const visibleTasks = allTasks - .filter((t) => !t.is_deleted) - .sort((a, b) => (a.due_date || '9999').localeCompare(b.due_date || '9999')); + const nonDeleted = allTasks.filter((t) => !t.is_deleted); + const visibleTasks = applySort(applyFilter(nonDeleted, currentFilter), currentSort); listEl.innerHTML = ''; if (visibleTasks.length === 0) { const lastSyncToken = await getMeta('last_sync_token'); - emptyEl.textContent = lastSyncToken - ? 'No tasks yet.' - : "No offline data yet — connect once while online to sync your tasks."; + if (!lastSyncToken) { + emptyEl.textContent = "No offline data yet — connect once while online to sync your tasks."; + } else { + emptyEl.textContent = 'No tasks here.'; + } emptyEl.hidden = false; listEl.hidden = true; return; @@ -121,6 +180,14 @@ async function renderOfflineTasks() { } } +function setActiveFilterButton(filter) { + document.querySelectorAll('#offline-filter-tabs button').forEach((btn) => { + const isActive = btn.dataset.filter === filter; + btn.classList.toggle('btn-primary', isActive); + btn.classList.toggle('btn-secondary', !isActive); + }); +} + document.addEventListener('DOMContentLoaded', () => { renderOfflineTasks(); @@ -136,4 +203,21 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + + document.querySelectorAll('#offline-filter-tabs button').forEach((btn) => { + btn.addEventListener('click', () => { + currentFilter = btn.dataset.filter; + setActiveFilterButton(currentFilter); + renderOfflineTasks(); + }); + }); + + const sortSelect = document.getElementById('offline-sort-select'); + if (sortSelect) { + sortSelect.value = currentSort; + sortSelect.addEventListener('change', () => { + currentSort = sortSelect.value; + renderOfflineTasks(); + }); + } }); diff --git a/templates/offline_tasks.html b/templates/offline_tasks.html index f5cf886..b09399b 100644 --- a/templates/offline_tasks.html +++ b/templates/offline_tasks.html @@ -20,12 +20,26 @@

Tasks (Offline)

+

You're offline. Changes you make here will sync automatically once you're back online.

+
+ + + + + +
+
diff --git a/templates/sw.js b/templates/sw.js index 3425ec2..1ada810 100644 --- a/templates/sw.js +++ b/templates/sw.js @@ -1,4 +1,4 @@ -{% load static %}const CACHE_NAME = 'keepitgoing-shell-v2'; +{% load static %}const CACHE_NAME = 'keepitgoing-shell-v3'; const OFFLINE_URL = '{% url "offline" %}'; const OFFLINE_TASKS_URL = '{% url "offline-tasks" %}';