Internal
Public Access
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:
co-authored by
Claude Sonnet 5
parent
19697fbc01
commit
fc045a0c4b
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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" %}';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user