Add HTML5 drag-and-drop within domain groups on the focus page. Items can be dragged to reorder within their domain; cross-domain drag is prevented. Uses hidden form POST (no fetch/XHR). Arrow buttons kept as fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
14 KiB
HTML
261 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1 class="page-title">Daily Focus</h1>
|
|
<div class="flex items-center gap-2">
|
|
{% if total_estimated %}<span class="text-sm text-muted">~{{ total_estimated }}min estimated</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<a href="/focus?focus_date={{ (focus_date|string)[:10] }}" class="btn btn-ghost btn-sm">Today</a>
|
|
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
|
|
</div>
|
|
|
|
<!-- Focus items grouped by domain -->
|
|
{% if items %}
|
|
<div class="card">
|
|
<table id="focus-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 %}
|
|
<tbody>
|
|
<tr><td colspan="9" style="padding:2px 8px;border-bottom:1px solid var(--border);background:var(--surface2);">
|
|
<span style="display:inline-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>
|
|
</span>
|
|
</td></tr>
|
|
</tbody>
|
|
<tbody class="focus-drag-group">
|
|
{% for item in domain.rows %}
|
|
<tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
|
|
<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>
|
|
<td style="padding:1px 1px;vertical-align:middle;">
|
|
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
|
<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>
|
|
</form>
|
|
</td>
|
|
<td style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<td style="padding:1px 1px;vertical-align:middle;">
|
|
<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>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
{% endfor %}
|
|
</table>
|
|
<form id="focus-reorder-form" action="/focus/reorder-all" method="post" style="display:none;">
|
|
<input type="hidden" name="item_ids" id="focus-reorder-ids">
|
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
|
{% endif %}
|
|
|
|
<!-- Add to Focus -->
|
|
<div class="card mt-3">
|
|
<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 -->
|
|
<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 %}"
|
|
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 %}"
|
|
class="tab-item {{ 'active' if current_source_type == 'list_items' }}">List Items</a>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<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="source_type" value="{{ current_source_type }}">
|
|
<input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px">
|
|
<select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()">
|
|
<option value="">All Domains</option>
|
|
{% for d in domains %}
|
|
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% if current_source_type == 'tasks' %}
|
|
<select name="area_id" class="filter-select" onchange="this.form.submit()">
|
|
<option value="">All Areas</option>
|
|
{% for a in areas %}
|
|
<option value="{{ a.id }}" {{ 'selected' if current_area_id == a.id|string }}>{{ a.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% endif %}
|
|
<select name="project_id" class="filter-select" id="focus-project" onchange="this.form.submit()">
|
|
<option value="">All Projects</option>
|
|
{% for p in projects %}
|
|
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit" class="btn btn-ghost btn-xs">Search</button>
|
|
{% if current_search or current_domain_id or current_area_id or current_project_id %}
|
|
<a href="/focus?focus_date={{ focus_date }}&source_type={{ current_source_type }}" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a>
|
|
{% endif %}
|
|
</form>
|
|
|
|
<!-- Available tasks -->
|
|
{% if current_source_type == 'tasks' %}
|
|
{% for t in available_tasks %}
|
|
<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="row-title">{{ t.title }}</span>
|
|
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
|
|
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
|
|
<form action="/focus/add" method="post" style="display:inline">
|
|
<input type="hidden" name="task_id" value="{{ t.id }}">
|
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
|
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available tasks matching filters</div>
|
|
{% endfor %}
|
|
{% if available_tasks|length > 25 %}
|
|
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
|
|
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_tasks|length }} tasks</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Available list items -->
|
|
{% elif current_source_type == 'list_items' %}
|
|
{% for li in available_list_items %}
|
|
<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 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 %}
|
|
<form action="/focus/add" method="post" style="display:inline">
|
|
<input type="hidden" name="list_item_id" value="{{ li.id }}">
|
|
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
|
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available list items matching filters</div>
|
|
{% endfor %}
|
|
{% if available_list_items|length > 25 %}
|
|
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
|
|
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_list_items|length }} items</button>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var domainSel = document.getElementById('focus-domain');
|
|
var projectSel = document.getElementById('focus-project');
|
|
var currentProjectId = '{{ current_project_id }}';
|
|
var form = document.getElementById('focus-filters');
|
|
|
|
if (domainSel) {
|
|
domainSel.addEventListener('change', function() {
|
|
var did = domainSel.value;
|
|
if (!did || !projectSel) { form.submit(); return; }
|
|
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(projects) {
|
|
projectSel.innerHTML = '<option value="">All Projects</option>';
|
|
projects.forEach(function(p) {
|
|
var opt = document.createElement('option');
|
|
opt.value = p.id;
|
|
opt.textContent = p.name;
|
|
if (p.id === currentProjectId) opt.selected = true;
|
|
projectSel.appendChild(opt);
|
|
});
|
|
form.submit();
|
|
})
|
|
.catch(function() { form.submit(); });
|
|
});
|
|
}
|
|
|
|
// Show more button
|
|
var showMoreBtn = document.getElementById('focus-show-more');
|
|
if (showMoreBtn) {
|
|
showMoreBtn.addEventListener('click', function() {
|
|
var hidden = document.querySelectorAll('.focus-hidden-item.hidden');
|
|
hidden.forEach(function(el) { el.classList.remove('hidden'); });
|
|
document.getElementById('focus-show-more-wrap').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Drag-and-drop reorder within domain groups
|
|
var dragRow = null;
|
|
var dragGroup = null;
|
|
|
|
document.querySelectorAll('.focus-drag-row').forEach(function(row) {
|
|
row.addEventListener('dragstart', function(e) {
|
|
dragRow = row;
|
|
dragGroup = row.closest('.focus-drag-group');
|
|
row.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', row.dataset.id);
|
|
});
|
|
|
|
row.addEventListener('dragend', function() {
|
|
row.classList.remove('dragging');
|
|
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
|
|
el.classList.remove('drag-over');
|
|
});
|
|
// Collect all IDs across all groups in DOM order and submit
|
|
if (dragRow) {
|
|
var allIds = [];
|
|
document.querySelectorAll('#focus-table .focus-drag-row').forEach(function(r) {
|
|
allIds.push(r.dataset.id);
|
|
});
|
|
document.getElementById('focus-reorder-ids').value = allIds.join(',');
|
|
document.getElementById('focus-reorder-form').submit();
|
|
}
|
|
dragRow = null;
|
|
dragGroup = null;
|
|
});
|
|
|
|
row.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
if (!dragRow || row === dragRow) return;
|
|
// Constrain to same domain group
|
|
if (row.closest('.focus-drag-group') !== dragGroup) return;
|
|
e.dataTransfer.dropEffect = 'move';
|
|
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
|
|
el.classList.remove('drag-over');
|
|
});
|
|
row.classList.add('drag-over');
|
|
});
|
|
|
|
row.addEventListener('dragleave', function() {
|
|
row.classList.remove('drag-over');
|
|
});
|
|
|
|
row.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
if (!dragRow || row === dragRow) return;
|
|
if (row.closest('.focus-drag-group') !== dragGroup) return;
|
|
row.classList.remove('drag-over');
|
|
// Insert dragged row before or after target based on position
|
|
var tbody = dragGroup;
|
|
var rows = Array.from(tbody.querySelectorAll('.focus-drag-row'));
|
|
var dragIdx = rows.indexOf(dragRow);
|
|
var targetIdx = rows.indexOf(row);
|
|
if (dragIdx < targetIdx) {
|
|
tbody.insertBefore(dragRow, row.nextSibling);
|
|
} else {
|
|
tbody.insertBefore(dragRow, row);
|
|
}
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|