Reports & calendar
Reports
The Reports page aggregates the whole org's entries over a date range. All aggregation happens server-side in one SQL pass (chains and rates resolve per Rules 1–2); trashed rows and running timers are always excluded.
Filters
- Range: This week / Last week / This month, plus a custom range with explicit from/to dates. The header subline restates the resolved range ("This week · Sep 7 – Sep 13 · 24 entries").
- Billable: All / Billable / Non-billable.
- Group by: Client / Project / Task / Tag — regroups both the chart and the breakdown table.
What you see
- Stat cards: Tracked (with worked-day count), Billable (% of tracked), Amount (with average $/h), Per day (average over worked days).
- Hours by day: a stacked column chart, one column per day, segments colored by the current grouping. Series colors are assigned by rank on the server and shared between the chart and the table chips, so the legend always matches.
- Breakdown table: color chip + label + parent sub-line, a share bar with %, entry count, hours, billable hours and amount, with a Total row. Unassigned time is real data, not hidden — "No client", "No project", "No task" and "Untagged" appear as their own rows.
#designand#dev appears in both rows — and the same fan-out applies to the chart's daily segments. The Total row and the stat cards count each entry exactly once, which is why the rows can add up to more than 100% of it. Every other grouping (client, project, task) is one row per entry.Export
Three buttons, three different artifacts:
| Button | What you get |
|---|---|
| CSV | GET /api/export/csv — one row per entry, for a spreadsheet. Columns: date, start, end, duration_h, name, task, project, client, tags, billable, rate, amount. Honors the range and the billable filter; grouping does not apply. RFC 4180 quoting, and fields starting with =, +, - or @ are neutralized so nothing executes on open |
| Export PDF | GET /api/export/pdf — a server-rendered A4 document: header with org name, range, billable filter and grouping; the four stat figures; an Hours by day table (hours, billable, amount per day, with a Total); a Breakdown by … table matching the current grouping; page numbers. Honors range, billable filter and grouping. No browser dialog, no headless Chrome — it is drawn with pdf-lib |
window.print() over a print stylesheet that drops the sidebar, timer bar and controls and remaps the theme to a light palette. Use it when you want the page exactly as it looks, or your browser's own "Save as PDF" |
Both downloads arrive as attachments with a dated filename — tick-export-2026-09-07--2026-09-13.csv, tick-report-2026-09-07_2026-09-13.pdf.
Calendar
The Calendar shows entries as blocks on a time grid — Week (7 columns, Monday start) or Day view, with ‹ Today › navigation. Narrow screens (under 1024px) open in Day view, since seven columns on a phone are unreadable; the segmented control still switches to Week if you want it. The base window is 7am–7pm; days with earlier or later entries extend the grid, and the body scrolls when it outgrows 12 hours. Each day header shows the weekday, the day number (accent ring on today) and that day's total; today gets a live "now" line.
Blocks carry the entry name and chain, with an accent edge when billable, plus a small play button on the right edge. Each running timer renders as a live block growing toward now — display-only until you stop it. Entries that overlap in time — two timers you ran together, or the entries they became once stopped — share the column side by side while each lane can still carry a name beside its play button: Day view, where a phone keeps three lanes and a desktop all ten. Where lanes would be too narrow (every overlap in Week view, whose columns are 100–160px wide), the whole group is drawn as one block listing its members by name, with +N when they don't all fit. Click it for a list of the entries — full name, chain, time range — each with its own play button, so you always read which entry you are restarting; a running timer in the group shows its live clock and taps into the timer bar. A block with nothing beside it keeps the full width. An entry that ran past midnight is drawn on both days: a head on the day it started, cut off at the bottom with a dashed edge, and a tail from midnight on the next day, cut at the top. Both open the same entry; the head is the part you drag or resize, and the day header's total counts the whole entry on the day it started.
Clicking and dragging
Everything snaps to 5 minutes, and Esc cancels an in-progress drag:
| Gesture | Effect |
|---|---|
| Click a block (no drag) | Opens that entry in the edit dialog — the same one the Time page uses, with description, chain, billable, date, start/end and tags. Saving updates the entry in place |
| Click the block's ▶ play button | Start again — copies name, reference and billable flag to the timer and starts it |
| Drag on empty space | Draws a ghost block, then opens the new-entry dialog prefilled with that day and time range |
| Drag a block | Moves it — across days too, in week view |
| Drag a block's top/bottom edge | Resizes it (changes start or end) |
| Click a folded block | Opens the list of entries it stands for, each with an edit row and its own ▶. Folded entries are moved or resized through the edit dialog — or in Day view, where they get lanes again |
Where a block is drawn, its play button is always visible rather than hover-only, and it swallows the pointer press so tapping it never starts a drag. A folded block has no play button on its face — nothing there is legible enough to restart by — so its list carries one per entry. Starting a timer is deliberately the narrow target: a bare block surface that silently begins tracking is too easy to hit by accident, so the wide target does the reversible thing (open the editor) instead.
Moves and resizes commit optimistically and revert if the server rejects them; duration and amount always come back from the server, so rate resolution stays correct after an edit. Edits made through the dialog re-fetch the visible range when it closes, so a changed time or chain redraws immediately.