Projects, clients & tasks

The open-ended catalog, the three data rules that drive it, and the trash.

Tick's catalog is deliberately open-ended: a task may belong to a project, a project may belong to a client, and any of the three can stand alone. A time entry can point at any one of them — or at nothing. Three rules make that model predictable.

Rule 1 — store only the deepest reference

An entry stores exactly one reference: a client, a project, a task, or nothing. Project and client are never copied onto the entry — they are derived at read time by walking task → task.project → project.client.

Consequences you will notice:

  • Attaching a task to the timer shows the whole chain (Task · Project · Client) as one chip, but only the task id is saved.
  • Reassigning a task to another project re-files its entire history automatically — every entry pointing at that task now reports under the new project and client. That is intended.
  • Deleting something an entry pointed at never destroys the time: the entry's reference is cleared instead (see Rule 3).

The database enforces this with a single ref_type / ref_id pair on time_entries (ref_type is one of client | project | task or null).

Rule 2 — rate inheritance

The billable rate for an entry is resolved server-side, first non-null wins:

entry.rate_override → task.rate → project.rate → client.rate → org_member.rate → user.default_rate
  • A task's own rate applies whether it belongs to a project or stands alone — it's the rung right below an entry-level override, and above the project it lives in. Clearing it falls back to the project → client → member → user chain, same as clearing a project's or client's rate.
  • billable defaults from the project's Billable by default flag when a project is resolvable, else true.
  • Amount = hours × resolved rate, computed only when the entry is billable.
  • Clients never compute inheritance — every API response carries the already-resolved rate (resolvedRate) and, for projects and tasks, where it came from (rateSource).

You can see the resolution at work on the Projects page: each project card's subline states its effective rate and its origin — $95/h (own rate), $110/h from client, or $85/h default (the user's default). A task with its own rate shows it next to the task's name; the timer bar and manual-entry dialog show the resolved rate on their billable toggles, and a per-member rate override slots into the chain for teams.

The Projects page

The header counts projects and open/done tasks, with New task and New project buttons. Each project is a collapsible card showing:

  • Client color dot, name, and a billable / internal tag.
  • Subline: client, x/y tasks open, and the rate with its source (Rule 2).
  • A progress bar of tracked vs estimated time ("6h 30m tracked / 40h estimated") — it switches to a brighter accent when over the estimate — plus the billable amount.

Expanding a card lists its tasks: a done toggle (completed tasks render struck-through and are hidden from the picker), entry count, tracked time, and a ▶ start button that starts the timer on that task. A task with its own rate shows $X/h right after its name. An + Add task link creates one inline. Standalone tasks (no project) get their own card below with the same row layout.

Project form: name, client picker, billable-by-default toggle, rate, estimate (free text — 40h, 2h 30m, same grammar as durations), visibility (private/public), archived. Task form: name, project picker, rate, estimate, completed — the rate field's placeholder shows what an empty field would inherit (the selected project's resolved rate and its source, or your default rate for a standalone task).

The Clients page

A table of clients: a round avatar with initials in the client's color (assigned deterministically from a short palette and reused for dots everywhere), the rate ($110/h, or the muted $85/h (default) when inheriting), project and task counts, tracked time with its amount, and edit / delete actions. The client form is just a name and an optional rate.

Rule 3 — cascade delete

Deleting a client (or project) opens a cascade dialog instead of a confirm button. It shows three checkboxes that cascade downward — Projects ⇒ Tasks ⇒ Time entries — each with a live count:

  • Checking Projects auto-checks Tasks and Entries; unchecking Entries unchecks the two above (you can't delete a container while keeping its contents checked-for-deletion).
  • An outcome panel spells out what happens to everything unchecked:
    • Projects kept → their client link is cleared and their rate falls back per Rule 2; the panel states the resulting $/h.
    • Tasks kept → if their project was deleted they become standalone, falling back to the project chain's rate unless the task has its own.
    • Entries kept → entries pointing directly at the deleted object have their reference cleared (ref_type = null). Time is never lost silently. Entries pointing at a surviving task or project are untouched.

Everything that was checked soft-deletes to the trash, and an undo toast restores the full snapshot in one click.

Rule 4 — deleting entries and tags

  • A single entry deletes immediately with an undo toast (configurable countdown, 8s by default).
  • Bulk delete confirms the count first, then trashes with undo.
  • Deleting a tag strips the label from entries and never touches time. Tag deletion is the one hard-ish case: it removes the tag but leaves every entry's duration and amount alone.

The Tags page

Tags are created inline in the page header (Enter works; names are lowercased and a leading # is stripped). Creating a name that already exists is a no-op — you get a "already exists" toast instead of a duplicate, and a name sitting in the trash is revived rather than recreated. The table shows each #tag chip, how many distinct projects/clients it is used on, entry count, hours, and last-used date. The filter action jumps to the Time page with #tag pre-filled; delete strips the label with an undo toast.

Tags are create/delete only — there is no rename, so there is no merge either. To consolidate two tags, filter the Time page by #tag, fix the tags field on each entry with ✎ Edit, then delete the tag you no longer want.

Trash

Everything deleted anywhere in Tick — clients, projects, tasks, tags, entries — is a soft delete (deleted_at timestamp) and lands in Settings → Trash, grouped by entity with a per-row "days left" countdown.

  • Items are restorable for 30 days; anything older is purged automatically the next time the trash is loaded.
  • Delete forever hard-deletes selected items (or the whole trash) immediately.
  • Undo toasts throughout the app restore exactly the rows a delete removed — including full cascade snapshots (the references a cascade cleared are put back too).
Delete forever and the automatic 30-day purge are both irreversible, and the trash is not a backup. Keep database backups if the time data matters.