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 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-09-04 23:20:27 -06:00
co-authored by Claude Sonnet 5
parent 19697fbc01
commit fc045a0c4b
3 changed files with 106 additions and 8 deletions
+91 -7
View File
@@ -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();
});
}
});
+14
View File
@@ -20,12 +20,26 @@
<div style="max-width: 640px; margin: 0 auto; padding: var(--space-lg);">
<div class="task-pane-header">
<h1 class="task-pane-title">Tasks (Offline)</h1>
<select id="offline-sort-select" class="form-select" style="width: auto;">
<option value="due_date">Due Date (Earliest)</option>
<option value="due_date_desc">Due Date (Latest)</option>
<option value="priority">Priority (High to Low)</option>
<option value="priority_low">Priority (Low to High)</option>
</select>
</div>
<p class="text-muted" style="margin-bottom: var(--space-lg);">
You're offline. Changes you make here will sync automatically once you're back online.
</p>
<div id="offline-filter-tabs" style="display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-lg);">
<button type="button" class="btn btn-sm btn-primary" data-filter="all">All Tasks</button>
<button type="button" class="btn btn-sm btn-secondary" data-filter="today">Today</button>
<button type="button" class="btn btn-sm btn-secondary" data-filter="upcoming">Upcoming</button>
<button type="button" class="btn btn-sm btn-secondary" data-filter="overdue">Overdue</button>
<button type="button" class="btn btn-sm btn-secondary" data-filter="completed">Completed</button>
</div>
<form id="offline-add-task-form" class="quick-add">
<input type="text" id="offline-task-title" class="quick-add-input" placeholder="Add a new task..." required>
<button type="submit" class="btn btn-primary">Add</button>
+1 -1
View File
@@ -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" %}';