Tier 3: timetracking CRUD + time tracking with topbar timer

This commit is contained in:
2026-02-28 04:48:37 +00:00
parent 6ad642084d
commit 7b259b9597
6 changed files with 497 additions and 0 deletions

View File

@@ -68,6 +68,10 @@
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Weblinks
</a>
<a href="/time" class="nav-item {{ 'active' if active_nav == 'time' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Time Log
</a>
<a href="/capture" class="nav-item {{ 'active' if active_nav == 'capture' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
Capture
@@ -127,6 +131,15 @@
<span class="topbar-env">DEV</span>
{% endif %}
<div class="topbar-spacer"></div>
<!-- Timer Pill (populated by JS) -->
<div id="timer-pill" class="timer-pill hidden">
<div class="timer-pill-dot"></div>
<a id="timer-pill-task" class="timer-pill-task" href="#"></a>
<span id="timer-pill-elapsed" class="timer-pill-elapsed">0:00</span>
<form method="POST" action="/time/stop" style="display:inline;">
<button type="submit" class="timer-pill-stop" title="Stop timer">&#9632;</button>
</form>
</div>
<button class="search-trigger" onclick="openSearch()" title="Search (Cmd/K)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<span>Search...</span>

155
templates/time_entries.html Normal file
View File

@@ -0,0 +1,155 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<div>
<h1 class="page-title">Time Log</h1>
<div class="text-muted text-sm mt-1">
{{ entries | length }} entries
&middot; {{ (total_minutes / 60) | round(1) }}h total
(last {{ days }} days)
</div>
</div>
</div>
<!-- Running timer banner -->
{% if running %}
<div class="card mb-3" style="border-color: var(--green); background: var(--green-soft);">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 10px; height: 10px; border-radius: 50%; background: var(--green); animation: pulse 1.5s infinite;"></div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text);">
Timer running: {{ running.task_title }}
</div>
{% if running.project_name %}
<div class="text-muted text-sm">{{ running.project_name }}</div>
{% endif %}
</div>
<div style="font-size: 1.25rem; font-weight: 700; font-family: var(--font-mono); color: var(--green);"
id="running-banner-elapsed" data-start="{{ running.start_at.isoformat() }}">
--:--
</div>
<form method="POST" action="/time/stop">
<input type="hidden" name="entry_id" value="{{ running.id }}">
<button type="submit" class="btn btn-danger btn-sm">Stop</button>
</form>
</div>
</div>
{% endif %}
<!-- Filters -->
<div class="filters-bar">
<a href="/time?days=1" class="btn {{ 'btn-primary' if days == 1 else 'btn-secondary' }} btn-sm">Today</a>
<a href="/time?days=7" class="btn {{ 'btn-primary' if days == 7 else 'btn-secondary' }} btn-sm">7 Days</a>
<a href="/time?days=30" class="btn {{ 'btn-primary' if days == 30 else 'btn-secondary' }} btn-sm">30 Days</a>
<a href="/time?days=90" class="btn {{ 'btn-primary' if days == 90 else 'btn-secondary' }} btn-sm">90 Days</a>
</div>
<!-- Daily totals summary -->
{% if daily_totals %}
<div class="card mb-3">
<div class="card-title mb-2">Daily Summary</div>
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
{% for day, mins in daily_totals | dictsort(reverse=true) %}
<div style="text-align: center; padding: 8px 14px; background: var(--surface2); border-radius: var(--radius); min-width: 80px;">
<div style="font-size: 1.1rem; font-weight: 700;">{{ (mins / 60) | round(1) }}h</div>
<div class="text-muted text-xs">{{ day }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Time entries -->
{% if entries %}
<div class="card">
{% set current_date = namespace(value='') %}
{% for entry in entries %}
{% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %}
{% if entry_date != current_date.value %}
<div style="padding: 10px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
{{ entry_date }}
</div>
{% set current_date.value = entry_date %}
{% endif %}
<div class="list-row">
{% if entry.end_at is none %}
<div style="width: 10px; height: 10px; border-radius: 50%; background: var(--green); animation: pulse 1.5s infinite; flex-shrink: 0;"></div>
{% endif %}
<div class="row-title">
<a href="/tasks/{{ entry.task_id }}">{{ entry.task_title }}</a>
</div>
{% if entry.project_name %}
<span class="row-tag">{{ entry.project_name }}</span>
{% endif %}
{% if entry.domain_name %}
<span class="row-domain-tag" style="background: {{ entry.domain_color or 'var(--accent)' }}20; color: {{ entry.domain_color or 'var(--accent)' }};">
{{ entry.domain_name }}
</span>
{% endif %}
<span style="font-family: var(--font-mono); font-size: 0.82rem; color: var(--text-secondary); min-width: 50px; text-align: right;">
{% if entry.end_at is none %}
<span style="color: var(--green);">running</span>
{% elif entry.duration_minutes %}
{% if entry.duration_minutes >= 60 %}
{{ (entry.duration_minutes / 60) | int }}h {{ entry.duration_minutes % 60 }}m
{% else %}
{{ entry.duration_minutes }}m
{% endif %}
{% else %}
--
{% endif %}
</span>
<span class="row-meta" style="min-width: 100px; text-align: right;">
{% if entry.start_at %}
{{ entry.start_at.strftime('%-I:%M %p') }}
{% if entry.end_at %}
- {{ entry.end_at.strftime('%-I:%M %p') }}
{% endif %}
{% endif %}
</span>
<div class="row-actions">
<form method="POST" action="/time/{{ entry.id }}/delete" data-confirm="Delete this time entry?">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red);">Delete</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-icon">&#9201;</div>
<div class="empty-state-text">No time entries in the last {{ days }} days</div>
<div class="text-muted text-sm">Start a timer from any task to begin tracking time</div>
</div>
{% endif %}
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
<script>
// Update running banner elapsed time
(function() {
const el = document.getElementById('running-banner-elapsed');
if (!el) return;
const startAt = new Date(el.dataset.start);
function update() {
const now = new Date();
const secs = Math.floor((now - startAt) / 1000);
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = secs % 60;
el.textContent = (h > 0 ? h + ':' : '') +
String(m).padStart(2, '0') + ':' +
String(s).padStart(2, '0');
}
update();
setInterval(update, 1000);
})();
</script>
{% endblock %}