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>
change rlvvzuywupwuxuknwvskknvnounzmopn
commit 3d4e783884d1de7c4be7cb5e23ad0e733748663d
author Alpha Chen <alpha@kejadlen.dev>
date
parent prltnxkm
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;