feat: focus page table layout with domain grouping, area/project columns, compact padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,8 +42,8 @@ async def focus_view(
|
|||||||
COALESCE(d.name, ld.name) as domain_name,
|
COALESCE(d.name, ld.name) as domain_name,
|
||||||
COALESCE(d.color, ld.color) as domain_color,
|
COALESCE(d.color, ld.color) as domain_color,
|
||||||
COALESCE(d.id, ld.id) as effective_domain_id,
|
COALESCE(d.id, ld.id) as effective_domain_id,
|
||||||
COALESCE(a.name, la.name) as area_name,
|
COALESCE(a.name, pa.name, la.name) as area_name,
|
||||||
COALESCE(a.id, la.id) as effective_area_id,
|
COALESCE(a.id, pa.id, la.id) as effective_area_id,
|
||||||
li.content as list_item_content, li.list_id as list_item_list_id,
|
li.content as list_item_content, li.list_id as list_item_list_id,
|
||||||
li.completed as list_item_completed,
|
li.completed as list_item_completed,
|
||||||
l.name as list_name
|
l.name as list_name
|
||||||
@@ -52,6 +52,7 @@ async def focus_view(
|
|||||||
LEFT JOIN projects p ON t.project_id = p.id
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
LEFT JOIN domains d ON t.domain_id = d.id
|
LEFT JOIN domains d ON t.domain_id = d.id
|
||||||
LEFT JOIN areas a ON t.area_id = a.id
|
LEFT JOIN areas a ON t.area_id = a.id
|
||||||
|
LEFT JOIN areas pa ON p.area_id = pa.id
|
||||||
LEFT JOIN list_items li ON df.list_item_id = li.id
|
LEFT JOIN list_items li ON df.list_item_id = li.id
|
||||||
LEFT JOIN lists l ON li.list_id = l.id
|
LEFT JOIN lists l ON li.list_id = l.id
|
||||||
LEFT JOIN projects lp ON l.project_id = lp.id
|
LEFT JOIN projects lp ON l.project_id = lp.id
|
||||||
@@ -62,7 +63,7 @@ async def focus_view(
|
|||||||
"""), {"target_date": target_date})
|
"""), {"target_date": target_date})
|
||||||
items = [dict(r._mapping) for r in result]
|
items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Build Domain > Area > Project hierarchy
|
# Group items by domain only — area/project shown as inline columns
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
domain_map = OrderedDict()
|
domain_map = OrderedDict()
|
||||||
for item in items:
|
for item in items:
|
||||||
@@ -70,41 +71,10 @@ async def focus_view(
|
|||||||
dl = item.get("domain_name") or "General"
|
dl = item.get("domain_name") or "General"
|
||||||
dc = item.get("domain_color") or ""
|
dc = item.get("domain_color") or ""
|
||||||
if dk not in domain_map:
|
if dk not in domain_map:
|
||||||
domain_map[dk] = {"label": dl, "color": dc, "areas": OrderedDict()}
|
domain_map[dk] = {"label": dl, "color": dc, "rows": []}
|
||||||
|
domain_map[dk]["rows"].append(item)
|
||||||
|
|
||||||
ak = item.get("effective_area_id") or "__none__"
|
hierarchy = list(domain_map.values())
|
||||||
al = item.get("area_name") or "General"
|
|
||||||
area_map = domain_map[dk]["areas"]
|
|
||||||
if ak not in area_map:
|
|
||||||
area_map[ak] = {"label": al, "projects": OrderedDict()}
|
|
||||||
|
|
||||||
pk = item.get("effective_project_id") or "__none__"
|
|
||||||
pl = item.get("project_name") or "General"
|
|
||||||
proj_map = area_map[ak]["projects"]
|
|
||||||
if pk not in proj_map:
|
|
||||||
proj_map[pk] = {"label": pl, "rows": []}
|
|
||||||
proj_map[pk]["rows"].append(item)
|
|
||||||
|
|
||||||
# Convert to nested lists for Jinja — "General" first, then alpha
|
|
||||||
def _sorted_projects(proj_dict):
|
|
||||||
general = []
|
|
||||||
named = []
|
|
||||||
for pk, pv in proj_dict.items():
|
|
||||||
entry = {"label": pv["label"], "rows": pv["rows"]}
|
|
||||||
if pk == "__none__":
|
|
||||||
general.append(entry)
|
|
||||||
else:
|
|
||||||
named.append(entry)
|
|
||||||
named.sort(key=lambda p: p["label"].lower())
|
|
||||||
return general + named
|
|
||||||
|
|
||||||
hierarchy = []
|
|
||||||
for dk, dv in domain_map.items():
|
|
||||||
domain_group = {"label": dv["label"], "color": dv["color"], "areas": []}
|
|
||||||
for ak, av in dv["areas"].items():
|
|
||||||
area_group = {"label": av["label"], "projects": _sorted_projects(av["projects"])}
|
|
||||||
domain_group["areas"].append(area_group)
|
|
||||||
hierarchy.append(domain_group)
|
|
||||||
|
|
||||||
# --- Available tasks ---
|
# --- Available tasks ---
|
||||||
available_tasks = []
|
available_tasks = []
|
||||||
|
|||||||
@@ -12,63 +12,45 @@
|
|||||||
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
|
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Focus items: Domain > Area > Project hierarchy -->
|
<!-- Focus items grouped by domain -->
|
||||||
{% if items %}
|
{% if items %}
|
||||||
|
<div class="card">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:24px"><col style="width:20px"><col style="width:74px"><col style="width:10px">
|
||||||
|
<col><col style="width:110px"><col style="width:120px"><col style="width:50px"><col style="width:24px">
|
||||||
|
</colgroup>
|
||||||
{% for domain in hierarchy %}
|
{% for domain in hierarchy %}
|
||||||
<div class="card mb-2">
|
<tr><td colspan="9" style="padding:2px 8px;border-bottom:1px solid var(--border);background:var(--surface2);">
|
||||||
<!-- Domain header -->
|
<span style="display:inline-flex;align-items:center;gap:6px;">
|
||||||
<div style="padding:4px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;">
|
<span style="width:8px;height:8px;border-radius:50%;background:{{ domain.color or 'var(--accent)' }};flex-shrink:0;"></span>
|
||||||
<span style="width:8px;height:8px;border-radius:50%;background:{{ domain.color or 'var(--accent)' }};flex-shrink:0;"></span>
|
<span style="font-weight:700;font-size:0.8rem;letter-spacing:0.03em;text-transform:uppercase;color:var(--text)">{{ domain.label }}</span>
|
||||||
<span style="font-weight:700;font-size:0.8rem;letter-spacing:0.03em;text-transform:uppercase;color:var(--text)">{{ domain.label }}</span>
|
</span>
|
||||||
</div>
|
</td></tr>
|
||||||
{% for area in domain.areas %}
|
{% for item in domain.rows %}
|
||||||
<!-- Area header -->
|
<tr style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
|
||||||
<div style="padding:3px 10px 3px 24px;border-bottom:1px solid var(--border);background:var(--surface2);">
|
<td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td>
|
||||||
<span style="font-weight:600;font-size:0.78rem;color:var(--muted)">{{ area.label }}</span>
|
<td style="padding:1px 1px;vertical-align:middle;">
|
||||||
</div>
|
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||||
{% for project in area.projects %}
|
<div class="row-check"><input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()"><label for="f-{{ item.id }}"></label></div>
|
||||||
<!-- Project header -->
|
</form>
|
||||||
<div style="padding:2px 10px 2px 40px;border-bottom:1px solid var(--border);">
|
</td>
|
||||||
<span style="font-weight:500;font-size:0.75rem;color:var(--muted)">{{ project.label }}</span>
|
<td style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
|
||||||
</div>
|
<td style="padding:1px 1px;vertical-align:middle;">{% if item.task_id %}<span class="priority-dot priority-{{ item.priority }}"></span>{% elif item.list_item_id %}<span style="color:var(--muted);font-size:0.85rem;">☰</span>{% endif %}</td>
|
||||||
{% for item in project.rows %}
|
<td style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% endif %}</td>
|
||||||
<div class="focus-item {{ 'completed' if item.completed }}" style="padding:3px 8px;min-height:0;">
|
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.area_name %}<span class="row-tag">{{ item.area_name }}</span>{% endif %}</td>
|
||||||
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}
|
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.task_id and item.project_name %}<span class="row-tag" style="background:var(--accent-soft);color:var(--accent)">{{ item.project_name }}</span>{% elif item.list_item_id and item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}</td>
|
||||||
{% include 'partials/reorder_arrows.html' %}
|
<td style="padding:1px 3px;vertical-align:middle;text-align:right;color:var(--muted);font-size:0.78rem;">{{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }}</td>
|
||||||
{% endwith %}
|
<td style="padding:1px 1px;vertical-align:middle;">
|
||||||
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
|
||||||
<div class="row-check">
|
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">×</button>
|
||||||
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
|
</form>
|
||||||
<label for="f-{{ item.id }}"></label>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</form>
|
|
||||||
{% if item.task_id %}
|
|
||||||
<span class="focus-meta" style="width:72px;min-width:72px;flex-shrink:0;">{{ item.due_date if item.due_date else ' '|safe }}</span>
|
|
||||||
<span class="priority-dot priority-{{ item.priority }}"></span>
|
|
||||||
{% if item.title %}
|
|
||||||
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="focus-title" style="color:var(--muted)">[Deleted]</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.estimated_minutes %}<span class="focus-meta">~{{ item.estimated_minutes }}min</span>{% endif %}
|
|
||||||
{% elif item.list_item_id %}
|
|
||||||
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">☰</span>
|
|
||||||
{% if item.list_item_content %}
|
|
||||||
<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="focus-title" style="color:var(--muted)">[Deleted]</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
|
|
||||||
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">×</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user