Tier 3: timetracking CRUD + time tracking with topbar timer
This commit is contained in:
@@ -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">■</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
155
templates/time_entries.html
Normal 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
|
||||
· {{ (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">⏱</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 %}
|
||||
Reference in New Issue
Block a user