Add urgency sorting with date/urgency toggle
Sort overdue tasks by how late they are relative to their interval,
so a daily task 3 days late ranks above a monthly task 3 days late.
Defaults to urgency; a toggle in the task header switches to date order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
diff --git a/lib/ketchup/models.rb b/lib/ketchup/models.rb
index c65b512..ee36254 100644
--- a/lib/ketchup/models.rb
+++ b/lib/ketchup/models.rb
@@ -41,6 +41,18 @@ end
class Task < Sequel::Model
many_to_one :series
+ INTERVAL_DAYS = {
+ "day" => 1, "week" => 7, "month" => 30, "quarter" => 91, "year" => 365
+ }.freeze
+
+ def urgency
+ days_overdue = Date.today - self[:due_date]
+ return 0 if days_overdue <= 0
+
+ interval = self[:interval_count] * INTERVAL_DAYS.fetch(self[:interval_unit])
+ days_overdue.to_f / interval
+ end
+
def complete!
DB.transaction do
update(completed_at: Time.now)
@@ -80,8 +92,5 @@ class Task < Sequel::Model
)
end
- def by_due_date
- order(Sequel[:tasks][:due_date])
- end
end
end
diff --git a/lib/ketchup/views/home.rb b/lib/ketchup/views/home.rb
index 69de8b3..45cdb1c 100644
--- a/lib/ketchup/views/home.rb
+++ b/lib/ketchup/views/home.rb
@@ -6,9 +6,10 @@ require_relative "layout"
module Views
class Home < Phlex::HTML
- def initialize(current_user:, tasks:)
+ def initialize(current_user:, tasks:, sort:)
@current_user = current_user
@tasks = tasks
+ @sort = sort
end
def view_template
@@ -55,7 +56,18 @@ module Views
end
div(class: "panel flow") do
- h2 { "Tasks" }
+ div(class: "task-header") do
+ h2 { "Tasks" }
+ nav(class: "sort-toggle") do
+ if @sort == :urgency
+ a(href: "/?sort=date") { "Date" }
+ span(class: "sort-active") { "Urgency" }
+ else
+ span(class: "sort-active") { "Date" }
+ a(href: "/?sort=urgency") { "Urgency" }
+ end
+ end
+ end
if @tasks.empty?
p(class: "empty") { "No tasks yet." }
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index 0f350a0..8b4b409 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -21,8 +21,14 @@ class Web < Roda
r.halt 403 unless current_user
r.root do
- tasks = Task.active.for_user(current_user).by_due_date.all
- Views::Home.new(current_user:, tasks:).call
+ sort = r.params["sort"] == "date" ? :date : :urgency
+ order = if sort == :urgency
+ ->(t) { [-t.urgency, t[:due_date]] }
+ else
+ ->(t) { t[:due_date] }
+ end
+ tasks = Task.active.for_user(current_user).all.sort_by(&order)
+ Views::Home.new(current_user:, tasks:, sort:).call
end
r.on "tasks", Integer do |task_id|
diff --git a/public/css/app.css b/public/css/app.css
index c5e2fa3..8d5b698 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -76,6 +76,31 @@ h1 { font-size: var(--step-2); }
/* Task list */
/* -------------------- */
+.task-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+}
+
+.sort-toggle {
+ display: flex;
+ gap: var(--space-2xs);
+ font-size: var(--step--1);
+}
+
+.sort-toggle a {
+ color: #888;
+ text-decoration: none;
+}
+
+.sort-toggle a:hover {
+ color: #1a1a1a;
+}
+
+.sort-active {
+ font-weight: 600;
+}
+
.task-list {
list-style: none;
padding: 0;