feat: domain > area > project hierarchy for daily focus with compact padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,11 @@ async def focus_view(
|
|||||||
t.project_id, t.due_date, t.estimated_minutes,
|
t.project_id, t.due_date, t.estimated_minutes,
|
||||||
COALESCE(p.name, lp.name) as project_name,
|
COALESCE(p.name, lp.name) as project_name,
|
||||||
COALESCE(t.project_id, l.project_id) as effective_project_id,
|
COALESCE(t.project_id, l.project_id) as effective_project_id,
|
||||||
d.name as domain_name, d.color as domain_color,
|
COALESCE(d.name, ld.name) as domain_name,
|
||||||
|
COALESCE(d.color, ld.color) as domain_color,
|
||||||
|
COALESCE(d.id, ld.id) as effective_domain_id,
|
||||||
|
COALESCE(a.name, la.name) as area_name,
|
||||||
|
COALESCE(a.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
|
||||||
@@ -47,24 +51,50 @@ async def focus_view(
|
|||||||
LEFT JOIN tasks t ON df.task_id = t.id
|
LEFT JOIN tasks t ON df.task_id = t.id
|
||||||
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 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
|
||||||
|
LEFT JOIN domains ld ON l.domain_id = ld.id
|
||||||
|
LEFT JOIN areas la ON l.area_id = la.id
|
||||||
WHERE df.focus_date = :target_date AND df.is_deleted = false
|
WHERE df.focus_date = :target_date AND df.is_deleted = false
|
||||||
ORDER BY df.sort_order, df.created_at
|
ORDER BY df.sort_order, df.created_at
|
||||||
"""), {"target_date": target_date})
|
"""), {"target_date": target_date})
|
||||||
items = [dict(r._mapping) for r in result]
|
items = [dict(r._mapping) for r in result]
|
||||||
|
|
||||||
# Group items by project for template rendering
|
# Build Domain > Area > Project hierarchy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
_groups = OrderedDict()
|
domain_map = OrderedDict()
|
||||||
for item in items:
|
for item in items:
|
||||||
key = item.get("effective_project_id") or "__general__"
|
dk = item.get("effective_domain_id") or "__none__"
|
||||||
label = item.get("project_name") or "General"
|
dl = item.get("domain_name") or "General"
|
||||||
if key not in _groups:
|
dc = item.get("domain_color") or ""
|
||||||
_groups[key] = {"label": label, "rows": []}
|
if dk not in domain_map:
|
||||||
_groups[key]["rows"].append(item)
|
domain_map[dk] = {"label": dl, "color": dc, "areas": OrderedDict()}
|
||||||
grouped_items = list(_groups.values())
|
|
||||||
|
ak = item.get("effective_area_id") or "__none__"
|
||||||
|
al = item.get("area_name") or ""
|
||||||
|
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 ""
|
||||||
|
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
|
||||||
|
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": []}
|
||||||
|
for pk, pv in av["projects"].items():
|
||||||
|
area_group["projects"].append({"label": pv["label"], "rows": pv["rows"]})
|
||||||
|
domain_group["areas"].append(area_group)
|
||||||
|
hierarchy.append(domain_group)
|
||||||
|
|
||||||
# --- Available tasks ---
|
# --- Available tasks ---
|
||||||
available_tasks = []
|
available_tasks = []
|
||||||
@@ -152,7 +182,7 @@ async def focus_view(
|
|||||||
|
|
||||||
return templates.TemplateResponse("focus.html", {
|
return templates.TemplateResponse("focus.html", {
|
||||||
"request": request, "sidebar": sidebar,
|
"request": request, "sidebar": sidebar,
|
||||||
"items": items, "grouped_items": grouped_items,
|
"items": items, "hierarchy": hierarchy,
|
||||||
"available_tasks": available_tasks,
|
"available_tasks": available_tasks,
|
||||||
"available_list_items": available_list_items,
|
"available_list_items": available_list_items,
|
||||||
"focus_date": target_date,
|
"focus_date": target_date,
|
||||||
|
|||||||
@@ -12,15 +12,31 @@
|
|||||||
<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 grouped by project -->
|
<!-- Focus items: Domain > Area > Project hierarchy -->
|
||||||
{% if items %}
|
{% if items %}
|
||||||
{% for group in grouped_items %}
|
{% for domain in hierarchy %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-2">
|
||||||
<div class="card-header" style="padding:6px 12px;">
|
<!-- Domain header -->
|
||||||
<h3 class="card-title" style="font-size:0.85rem;margin:0;">{{ group.label }}</h3>
|
<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="font-weight:700;font-size:0.8rem;letter-spacing:0.03em;text-transform:uppercase;color:var(--text)">{{ domain.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% for item in group.rows %}
|
{% for area in domain.areas %}
|
||||||
<div class="focus-item {{ 'completed' if item.completed }}">
|
{% if area.label %}
|
||||||
|
<!-- Area header -->
|
||||||
|
<div style="padding:3px 10px 3px 24px;border-bottom:1px solid var(--border);background:var(--surface2);">
|
||||||
|
<span style="font-weight:600;font-size:0.78rem;color:var(--muted)">{{ area.label }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% for project in area.projects %}
|
||||||
|
{% if project.label %}
|
||||||
|
<!-- Project header -->
|
||||||
|
<div style="padding:2px 10px 2px 40px;border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-weight:500;font-size:0.75rem;color:var(--accent)">{{ project.label }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% for item in project.rows %}
|
||||||
|
<div class="focus-item {{ 'completed' if item.completed }}" style="padding:3px 8px;min-height:0;">
|
||||||
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}
|
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}
|
||||||
{% include 'partials/reorder_arrows.html' %}
|
{% include 'partials/reorder_arrows.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
@@ -53,6 +69,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -60,11 +78,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Add to Focus -->
|
<!-- Add to Focus -->
|
||||||
<div class="card mt-4">
|
<div class="card mt-3">
|
||||||
<div class="card-header"><h2 class="card-title">Add to Focus</h2></div>
|
<div class="card-header" style="padding:4px 10px;"><h2 class="card-title" style="font-size:0.85rem;margin:0;">Add to Focus</h2></div>
|
||||||
|
|
||||||
<!-- Tab strip -->
|
<!-- Tab strip -->
|
||||||
<div class="tab-strip" style="padding: 0 12px;">
|
<div class="tab-strip" style="padding: 0 10px;">
|
||||||
<a href="/focus?focus_date={{ focus_date }}&source_type=tasks{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
<a href="/focus?focus_date={{ focus_date }}&source_type=tasks{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
||||||
class="tab-item {{ 'active' if current_source_type == 'tasks' }}">Tasks</a>
|
class="tab-item {{ 'active' if current_source_type == 'tasks' }}">Tasks</a>
|
||||||
<a href="/focus?focus_date={{ focus_date }}&source_type=list_items{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
<a href="/focus?focus_date={{ focus_date }}&source_type=list_items{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
|
||||||
@@ -72,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 8px 12px; border-bottom: 1px solid var(--border);">
|
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 4px 10px; border-bottom: 1px solid var(--border);">
|
||||||
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||||
<input type="hidden" name="source_type" value="{{ current_source_type }}">
|
<input type="hidden" name="source_type" value="{{ current_source_type }}">
|
||||||
<input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px">
|
<input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px">
|
||||||
@@ -105,7 +123,7 @@
|
|||||||
<!-- Available tasks -->
|
<!-- Available tasks -->
|
||||||
{% if current_source_type == 'tasks' %}
|
{% if current_source_type == 'tasks' %}
|
||||||
{% for t in available_tasks %}
|
{% for t in available_tasks %}
|
||||||
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}">
|
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}" style="padding:3px 8px;min-height:0;">
|
||||||
<span class="priority-dot priority-{{ t.priority }}"></span>
|
<span class="priority-dot priority-{{ t.priority }}"></span>
|
||||||
<span class="row-title">{{ t.title }}</span>
|
<span class="row-title">{{ t.title }}</span>
|
||||||
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
||||||
@@ -117,18 +135,18 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available tasks matching filters</div>
|
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available tasks matching filters</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if available_tasks|length > 25 %}
|
{% if available_tasks|length > 25 %}
|
||||||
<div style="padding: 8px 12px; text-align:center;" id="focus-show-more-wrap">
|
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
|
||||||
<button class="btn btn-ghost btn-sm" id="focus-show-more">Show all {{ available_tasks|length }} tasks</button>
|
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_tasks|length }} tasks</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Available list items -->
|
<!-- Available list items -->
|
||||||
{% elif current_source_type == 'list_items' %}
|
{% elif current_source_type == 'list_items' %}
|
||||||
{% for li in available_list_items %}
|
{% for li in available_list_items %}
|
||||||
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}">
|
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}" style="padding:3px 8px;min-height:0;">
|
||||||
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">☰</span>
|
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">☰</span>
|
||||||
<span class="row-title">{{ li.content }}</span>
|
<span class="row-title">{{ li.content }}</span>
|
||||||
{% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %}
|
{% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %}
|
||||||
@@ -139,11 +157,11 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available list items matching filters</div>
|
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available list items matching filters</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if available_list_items|length > 25 %}
|
{% if available_list_items|length > 25 %}
|
||||||
<div style="padding: 8px 12px; text-align:center;" id="focus-show-more-wrap">
|
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
|
||||||
<button class="btn btn-ghost btn-sm" id="focus-show-more">Show all {{ available_list_items|length }} items</button>
|
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_list_items|length }} items</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user