feat: eisenhower matrix view
This commit is contained in:
2
main.py
2
main.py
@@ -42,6 +42,7 @@ from routers import (
|
||||
processes as processes_router,
|
||||
calendar as calendar_router,
|
||||
time_budgets as time_budgets_router,
|
||||
eisenhower as eisenhower_router,
|
||||
)
|
||||
|
||||
|
||||
@@ -199,3 +200,4 @@ app.include_router(time_tracking_router.router)
|
||||
app.include_router(processes_router.router)
|
||||
app.include_router(calendar_router.router)
|
||||
app.include_router(time_budgets_router.router)
|
||||
app.include_router(eisenhower_router.router)
|
||||
|
||||
76
routers/eisenhower.py
Normal file
76
routers/eisenhower.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Eisenhower Matrix: read-only 2x2 priority/urgency grid of open tasks."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
|
||||
from core.database import get_db
|
||||
from core.sidebar import get_sidebar_data
|
||||
|
||||
router = APIRouter(prefix="/eisenhower", tags=["eisenhower"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def eisenhower_matrix(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
result = await db.execute(text("""
|
||||
SELECT t.id, t.title, t.priority, t.status, t.due_date,
|
||||
t.context, t.estimated_minutes,
|
||||
p.name as project_name,
|
||||
d.name as domain_name, d.color as domain_color
|
||||
FROM tasks t
|
||||
LEFT JOIN projects p ON t.project_id = p.id
|
||||
LEFT JOIN domains d ON t.domain_id = d.id
|
||||
WHERE t.is_deleted = false
|
||||
AND t.status IN ('open', 'in_progress', 'blocked')
|
||||
ORDER BY t.priority, t.due_date NULLS LAST, t.title
|
||||
"""))
|
||||
tasks = [dict(r._mapping) for r in result]
|
||||
|
||||
# Classify into quadrants
|
||||
from datetime import date, timedelta
|
||||
today = date.today()
|
||||
urgent_cutoff = today + timedelta(days=7)
|
||||
|
||||
quadrants = {
|
||||
"do_first": [], # Urgent + Important
|
||||
"schedule": [], # Not Urgent + Important
|
||||
"delegate": [], # Urgent + Not Important
|
||||
"eliminate": [], # Not Urgent + Not Important
|
||||
}
|
||||
|
||||
for t in tasks:
|
||||
important = t["priority"] in (1, 2)
|
||||
urgent = (
|
||||
t["due_date"] is not None
|
||||
and t["due_date"] <= urgent_cutoff
|
||||
)
|
||||
|
||||
if important and urgent:
|
||||
quadrants["do_first"].append(t)
|
||||
elif important and not urgent:
|
||||
quadrants["schedule"].append(t)
|
||||
elif not important and urgent:
|
||||
quadrants["delegate"].append(t)
|
||||
else:
|
||||
quadrants["eliminate"].append(t)
|
||||
|
||||
counts = {k: len(v) for k, v in quadrants.items()}
|
||||
total = sum(counts.values())
|
||||
|
||||
return templates.TemplateResponse("eisenhower.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"quadrants": quadrants,
|
||||
"counts": counts,
|
||||
"total": total,
|
||||
"today": today,
|
||||
"page_title": "Eisenhower Matrix",
|
||||
"active_nav": "eisenhower",
|
||||
})
|
||||
147
static/style.css
147
static/style.css
@@ -1437,3 +1437,150 @@ a:hover { color: var(--accent-hover); }
|
||||
.cal-event-time { display: none; }
|
||||
.cal-month-label { font-size: 1rem; min-width: 140px; }
|
||||
}
|
||||
|
||||
/* ---- Eisenhower Matrix ---- */
|
||||
.eisenhower-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr auto;
|
||||
gap: 12px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.eisenhower-y-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.eisenhower-x-spacer {
|
||||
/* empty cell under the y-labels */
|
||||
}
|
||||
|
||||
.eisenhower-x-label {
|
||||
text-align: center;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.eisenhower-quadrant {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.eisenhower-q1 { border-top: 3px solid var(--red); }
|
||||
.eisenhower-q2 { border-top: 3px solid var(--accent); }
|
||||
.eisenhower-q3 { border-top: 3px solid var(--amber); }
|
||||
.eisenhower-q4 { border-top: 3px solid var(--muted); }
|
||||
|
||||
.eisenhower-quadrant-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.eisenhower-quadrant-header h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.eisenhower-quadrant-subtitle {
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.eisenhower-task-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.eisenhower-task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.eisenhower-task:hover {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.eisenhower-task-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eisenhower-task-due {
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eisenhower-task-due.overdue {
|
||||
color: var(--red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.eisenhower-task-project {
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
background: var(--surface2);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eisenhower-empty {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-red { background: var(--red); color: #fff; }
|
||||
.badge-amber { background: var(--amber); color: #fff; }
|
||||
.badge-accent { background: var(--accent); color: #fff; }
|
||||
.badge-muted { background: var(--muted); color: #fff; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.eisenhower-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
.eisenhower-y-label,
|
||||
.eisenhower-x-label,
|
||||
.eisenhower-x-spacer { display: none; }
|
||||
.eisenhower-quadrant { min-height: 120px; }
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><rect x="7" y="14" width="3" height="3" rx="0.5"/><rect x="14" y="14" width="3" height="3" rx="0.5"/></svg>
|
||||
Calendar
|
||||
</a>
|
||||
<a href="/eisenhower" class="nav-item {{ 'active' if active_nav == 'eisenhower' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg>
|
||||
Eisenhower
|
||||
</a>
|
||||
<a href="/decisions" class="nav-item {{ 'active' if active_nav == 'decisions' }}">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9 12l2 2 4-4"/></svg>
|
||||
Decisions
|
||||
|
||||
129
templates/eisenhower.html
Normal file
129
templates/eisenhower.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Eisenhower Matrix</h1>
|
||||
<span class="text-muted">{{ total }} open tasks classified by priority & urgency</span>
|
||||
</div>
|
||||
|
||||
<div class="eisenhower-grid">
|
||||
<!-- Axis labels -->
|
||||
<div class="eisenhower-y-label">
|
||||
<span>Important</span>
|
||||
</div>
|
||||
|
||||
<!-- Q1: Urgent + Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q1">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Do First</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Urgent & Important</span>
|
||||
<span class="badge badge-red">{{ counts.do_first }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.do_first %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
|
||||
{{ task.due_date.strftime('%b %-d') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Q2: Not Urgent + Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q2">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Schedule</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Not Urgent & Important</span>
|
||||
<span class="badge badge-accent">{{ counts.schedule }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.schedule %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second row y-label -->
|
||||
<div class="eisenhower-y-label">
|
||||
<span>Not Important</span>
|
||||
</div>
|
||||
|
||||
<!-- Q3: Urgent + Not Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q3">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Delegate</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Urgent & Not Important</span>
|
||||
<span class="badge badge-amber">{{ counts.delegate }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.delegate %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due {% if task.due_date < today %}overdue{% endif %}">
|
||||
{{ task.due_date.strftime('%b %-d') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Q4: Not Urgent + Not Important -->
|
||||
<div class="eisenhower-quadrant eisenhower-q4">
|
||||
<div class="eisenhower-quadrant-header">
|
||||
<h3>Eliminate</h3>
|
||||
<span class="eisenhower-quadrant-subtitle">Not Urgent & Not Important</span>
|
||||
<span class="badge badge-muted">{{ counts.eliminate }}</span>
|
||||
</div>
|
||||
<div class="eisenhower-task-list">
|
||||
{% for task in quadrants.eliminate %}
|
||||
<a href="/tasks/{{ task.id }}" class="eisenhower-task">
|
||||
<span class="priority-dot priority-{{ task.priority }}"></span>
|
||||
<span class="eisenhower-task-title">{{ task.title }}</span>
|
||||
{% if task.due_date %}
|
||||
<span class="eisenhower-task-due">{{ task.due_date.strftime('%b %-d') }}</span>
|
||||
{% endif %}
|
||||
{% if task.project_name %}
|
||||
<span class="eisenhower-task-project">{{ task.project_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="eisenhower-empty">No tasks</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- X-axis labels -->
|
||||
<div class="eisenhower-x-spacer"></div>
|
||||
<div class="eisenhower-x-label">Urgent</div>
|
||||
<div class="eisenhower-x-label">Not Urgent</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user