Skip to content

feat(timer): start with project and task pickers, save and stop with a description - #13

Closed
gdarko wants to merge 6 commits into
feat/task-centric-polishfrom
feat/timer-dialogs
Closed

gdarko wants to merge 6 commits into
feat/task-centric-polishfrom
feat/timer-dialogs

Conversation

@gdarko

@gdarko gdarko commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Stopping a timer used to write the interval immediately, with whatever had been
typed at start, which for most people was nothing: the header chip and the
floating launcher stopped on one click and rows, cards, the task page and the
time log had no way to describe the time at all. Starting from the floating
circle offered a bare task search that only worked for someone who already knew
the task existed. Both ends are proper dialogs now.

Save and stop

Every stop control opens the same dialog: the header chip, the launcher, a task
row, a card, the task page and the time log, whose running rows grow a stop of
their own. It shows the task, the ticking elapsed time and what the entry will
read after the company's rounding, with the description and the billable flag
prefilled from the running entry. Discard confirms inline, naming the time it is
about to throw away, instead of through a browser confirm. Cancel leaves the
clock running and writes nothing.

"Stop and start" on another row runs the same dialog for the time being left
behind, and starts the new task only when that time was saved.

Start a timer

The floating circle's idle click asks the way people think about it: the
project, then the task inside it, and when the search matches nothing the typed
text becomes "Create task ... and start". The description and the billable flag
come along, prefilled from the task. The launcher's inline search panel and its
separate discard go with it; the running panel keeps what it is good at, saying
what is on the clock with the task and the stop beside it.

Backend

Both stop routes take an optional description and billable flag, applied only
when sent, so a client that stops with an empty body still keeps what the start
recorded. Both start routes take the billable override, and starting the task
the clock is already on stops being a conflict: it applies the details and
answers the running entry, which is what makes "create this task and start it"
correct when auto_start_tasks has already opened the clock on create. A start
on a different task is still a 409, and a mismatched stop is still refused
before anything is written.

support/time.ts gains roundMinutes, mirroring Application\Rounding
exactly, so the dialog's preview is the number the entry gets.

No schema change.

Verification

  • vendor/bin/pint, composer run lint, composer run test: 328 tests, 1376
    assertions (314 before, 14 new covering description and billable at stop, the
    billable override at start, the idempotent same-task start and the mismatch
    that changes nothing).
  • Both validators, pnpm run lint, pnpm exec tsc --noEmit, pnpm run build
    (one window.__invoiceshelf_vue, no bare vue imports).
  • Browser pass on the live stack as the demo user: start from the circle with a
    project and a task; stop from the chip with an edited description and billable
    off, landing on the time-log row; "Create task ... and start" both with
    auto_start_tasks off and on (one entry either way, the description applied);
    stop from a row with cancel and with discard; stop from the time log; the
    "stop and start" chain; the dialog on a settings page. Console clean.

https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E

…d stop

Stopping wrote the interval with whatever had been typed at start, which for
most people was nothing, so the time log filled up with unnamed hours. Both
stop routes now take an optional description and billable flag and apply each
only when it was sent, so a client that stops with an empty body still keeps
what the start recorded.

Starting gains the same billable override, and starting the task the clock is
already on stops being a conflict: it applies the details it carries and
answers the running entry. That is what makes "create this task and start it"
correct when auto_start_tasks has already opened the clock on create. A start
on a different task is still the 409 the run control turns into "stop that one
and start this one", and a mismatched stop is still refused before anything is
written.
The store gains the two questions a timer ever asks, held as promises the way
the invoicing store holds its number prompt: one dialog mounted in the company
layout answers whatever control asked. stopWithPrompt is now the only way a
timer is stopped, so the dialog, the discard and the single success message
are written once instead of by each of the four callers that used to repeat
them; startWithPrompt creates the task first when the answer was a name rather
than a task.

roundMinutes mirrors Application\Rounding exactly, so the dialog can promise
what the entry is about to say: zero stays zero, nearest bills a spell shorter
than one increment as a whole one, and down is the one direction that may
answer zero for real work.
Save and stop shows the task, the ticking clock and what the entry will read
after the company's rounding, with the description and the billable flag
prefilled from the running entry. Discard confirms inline, naming the time it
is about to throw away, rather than through a browser confirm.

Start a timer asks the way people think about it: the project, then the task
inside it, and when the search matches nothing the typed text becomes "create
this task and start", so a name in someone's head does not have to become a
form first.

Both are mounted beside the invoice number dialog rather than by each control
that opens one, and neither is hidden on any path: a clock running while
someone is in the settings still has to be stoppable there. The launcher steps
aside while either is open, because a floating button over a modal backdrop
reads as something still to be pressed.
The chip, the launcher, a task row and the "stop and start" chain all go
through stopWithPrompt now, and the running rows of a task's time log grow a
stop of their own, so the log is no longer the one place a running entry
cannot be closed. Another member's running row still only says so.

The launcher's idle click opens the start dialog, which takes its inline
search panel and its separate discard with it: the panel now only has the one
job it is good at, saying what is on the clock and offering the task and the
stop beside it.
Carries the timer dialogs, the prompt-driven store and the call sites that go
through it.
…orner

The idle launcher was sized by padding around its icon, so it rendered
wider than tall; it is now a fixed 56px circle, and only grows into a
pill while a timer runs and the elapsed time is shown. It also sits
closer to the corner so it overlaps less content.
@gdarko

gdarko commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #14, which carries this stack consolidated into three commits on top of main.

@gdarko gdarko closed this Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant