Add day-grouped timesheet with day accept and commit history

This commit is contained in:
2026-09-14 15:04:55 +02:00
parent dcd2e97a1d
commit 75e47d7f40
4 changed files with 124 additions and 20 deletions
+32 -4
View File
@@ -1,10 +1,38 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Timesheets · Time Track Server</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"><style>small.suggested{color:#9a6700}.rejected{opacity:.5}td{vertical-align:top}form.inline{display:inline}</style></head>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Timesheets · Time Track Server</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"><style>small.suggested{color:#9a6700}.rejected{opacity:.5}td{vertical-align:top}form.inline{display:inline;margin:0}.day{margin-bottom:1.5rem;padding-top:.75rem;border-top:2px solid var(--pico-muted-border-color)}.day-header{display:flex;gap:1rem;align-items:baseline;flex-wrap:wrap;margin-bottom:.25rem}.day-header h2{margin:0;font-size:1.1rem}.day-total{color:var(--pico-muted-color)}details{margin-bottom:.5rem}</style></head>
<body><main class="container"><nav><ul><li><strong>Time Track Server</strong></li></ul><ul><li>{{ timezone }}</li><li><form class="inline" method="post" action="/logout"><button class="secondary outline">Sign out</button></form></li></ul></nav>
<h1>Combined timesheet</h1><form method="get" class="grid"><label>From <input type="date" name="start" value="{{ start }}"></label><label>To <input type="date" name="end" value="{{ end }}"></label><button type="submit">Filter</button></form>
<section class="grid"><article><header>Accepted total</header><strong>{{ format_seconds(accepted) }}</strong></article><article><header>Suggested, not counted</header><strong>{{ format_seconds(suggested) }}</strong></article></section>
<details><summary>Add manual time</summary><form method="post" action="/entries/manual" class="grid"><label>Start <input name="start_at" type="datetime-local" required></label><label>End <input name="end_at" type="datetime-local" required></label><label>Task <input name="task"></label><label>Project <input name="project_slug"></label><button type="submit">Add accepted time</button></form></details>
<details><summary>Upload mobile-provider call log (PDF)</summary><p>Uploads are extracted and retained for review. A provider-specific parser is added after a redacted sample statement is available.</p><form method="post" action="/call-imports" enctype="multipart/form-data"><input type="file" name="file" accept="application/pdf,.pdf" required><button type="submit">Upload PDF</button></form></details>
<h2>Entries</h2><figure><table><thead><tr><th>Start</th><th>End</th><th>Source</th><th>Task / project</th><th>Duration</th><th>Status</th><th>Actions</th></tr></thead><tbody>
{% for entry in entries %}<tr class="{{ entry.status }}"><td>{{ entry.start_at.astimezone(display_tz).strftime('%Y-%m-%d %H:%M') }}</td><td>{{ entry.end_at.astimezone(display_tz).strftime('%H:%M') }}</td><td>{{ entry.source }}{% if entry.kind == 'zed_inferred' %}<br><small class="suggested">inferred{% if overlaps_accepted(entry) %}; overlaps accepted time{% endif %}</small>{% endif %}</td><td>{{ entry.task or '—' }}{% if entry.project_slug %}<br><small>{{ entry.project_slug }}</small>{% endif %}</td><td>{{ format_seconds(entry.duration_seconds) }}</td><td>{{ entry.status }}</td><td>{% if entry.status == 'suggested' %}<form class="inline" method="post" action="/entries/{{ entry.id }}/status"><input type="hidden" name="status" value="accepted"><button>Accept</button></form><form class="inline" method="post" action="/entries/{{ entry.id }}/status"><input type="hidden" name="status" value="rejected"><button class="secondary">Reject</button></form>{% endif %}<details><summary>Edit</summary><form method="post" action="/entries/{{ entry.id }}/edit"><label>Start <input name="start_at" type="datetime-local" value="{{ entry.start_at.astimezone(display_tz).strftime('%Y-%m-%dT%H:%M') }}" required></label><label>End <input name="end_at" type="datetime-local" value="{{ entry.end_at.astimezone(display_tz).strftime('%Y-%m-%dT%H:%M') }}" required></label><label>Task <input name="task" value="{{ entry.task }}"></label><label>Project <input name="project_slug" value="{{ entry.project_slug or '' }}"></label><label>Notes <textarea name="notes">{{ entry.notes }}</textarea></label><button type="submit">Save</button></form></details></td></tr>
{% else %}<tr><td colspan="7">No entries yet. Sync a tracker or add manual time.</td></tr>{% endfor %}</tbody></table></figure></main></body></html>
<h2>Days</h2>
{% for day in days %}
<section class="day">
<header class="day-header">
<h2>{{ day.date.strftime('%a %d %b %Y') }}</h2>
<span class="day-total">accepted <strong>{{ format_seconds(day.accepted) }}</strong> · suggested <span class="suggested">{{ format_seconds(day.suggested) }}</span>{% if day.commits %} · {{ day.commits|length }} commit{{ 's' if day.commits|length != 1 }}{% endif %}</span>
{% if day.suggested %}<form class="inline" method="post" action="/days/{{ day.date.isoformat() }}/accept"><input type="hidden" name="start" value="{{ start }}"><input type="hidden" name="end" value="{{ end }}"><button>Accept day</button></form>{% endif %}
</header>
{% if day.entries %}
<details><summary>Show {{ day.entries|length }} entr{{ 'y' if day.entries|length == 1 else 'ies' }}</summary>
<figure><table><thead><tr><th>Start</th><th>End</th><th>Source</th><th>Task / project</th><th>Duration</th><th>Status</th><th>Actions</th></tr></thead><tbody>
{% for entry in day.entries %}
<tr class="{{ entry.status }}"><td>{{ entry.start_at.astimezone(display_tz).strftime('%H:%M') }}</td><td>{{ entry.end_at.astimezone(display_tz).strftime('%H:%M') }}</td><td>{{ entry.source }}{% if entry.kind == 'zed_inferred' %}<br><small class="suggested">inferred{% if overlaps_accepted(entry) %}; overlaps accepted time{% endif %}</small>{% endif %}</td><td>{{ entry.task or '—' }}{% if entry.project_slug %}<br><small>{{ entry.project_slug }}</small>{% endif %}</td><td>{{ format_seconds(entry.duration_seconds) }}</td><td>{{ entry.status }}</td><td>{% if entry.status == 'suggested' %}<form class="inline" method="post" action="/entries/{{ entry.id }}/status"><input type="hidden" name="status" value="accepted"><input type="hidden" name="start" value="{{ start }}"><input type="hidden" name="end" value="{{ end }}"><button>Accept</button></form>{% endif %}{% if entry.status != 'rejected' %}<form class="inline" method="post" action="/entries/{{ entry.id }}/status"><input type="hidden" name="status" value="rejected"><input type="hidden" name="start" value="{{ start }}"><input type="hidden" name="end" value="{{ end }}"><button class="secondary">Reject</button></form>{% endif %}<details><summary>Edit</summary><form method="post" action="/entries/{{ entry.id }}/edit"><label>Start <input name="start_at" type="datetime-local" value="{{ entry.start_at.astimezone(display_tz).strftime('%Y-%m-%dT%H:%M') }}" required></label><label>End <input name="end_at" type="datetime-local" value="{{ entry.end_at.astimezone(display_tz).strftime('%Y-%m-%dT%H:%M') }}" required></label><label>Task <input name="task" value="{{ entry.task }}"></label><label>Project <input name="project_slug" value="{{ entry.project_slug or '' }}"></label><label>Notes <textarea name="notes">{{ entry.notes }}</textarea></label><button type="submit">Save</button></form></details></td></tr>
{% endfor %}
</tbody></table></figure>
</details>
{% else %}<p><small>No tracked entries this day.</small></p>{% endif %}
{% if day.commits %}
<details><summary>Git commits ({{ day.commits|length }})</summary>
<figure><table><thead><tr><th>Time</th><th>Project</th><th>Author</th><th>Commit</th></tr></thead><tbody>
{% for commit in day.commits %}
<tr><td>{{ commit.occurred_at.astimezone(display_tz).strftime('%H:%M') }}</td><td>{{ commit.project_slug }}</td><td>{{ commit.payload.get('author', '') }}</td><td><code>{{ (commit.payload.get('sha', '') or '')[:7] }}</code> {{ commit.payload.get('subject', '') }}</td></tr>
{% endfor %}
</tbody></table></figure>
</details>
{% endif %}
</section>
{% else %}
<p>No entries yet. Sync a tracker or add manual time.</p>
{% endfor %}
</main></body></html>