feat(time): join work-item names (ticket/project/task) into the time-entry response #250

Merged
nrupard merged 1 commit from feat/PMS-332-time-entry-work-item-names into main 2026-06-15 21:27:37 +02:00
Owner

Summary

The time-entries list could only render a bare Ticket/Project/- label because TimeEntryResponse carried ids but no work-item names. Task names in particular are not client-resolvable (there is no global tasks list endpoint, only /projects/{id}/tasks per project), so the rollup must come from the server. MAPPS-168 needs the real name, including the task title, on the list.

Changes

  • Add four nullable fields to TimeEntryResponse and the internal TimeEntryRow: ticket_number, ticket_title, project_name, task_title (all Option<String>), populated in the From<TimeEntryRow> impl.
  • In both read paths (list_time_entries, get_time_entry), add LEFT JOIN tickets / projects / tasks and select tk.ticket_number, tk.title AS ticket_title, pr.name AS project_name, ta.title AS task_title.
  • Because the joined tables also carry tenant_id/id/created_at, the list query's data predicates and ORDER BY are qualified with the te alias to avoid ambiguous-column errors. The count query has no join and stays on bare columns.
  • Existing fields, filters, tenant scoping, and the single-query (no N+1) shape are unchanged.

Tenant safety

time_entries.project_id / task_id are bare UUIDs (no FK). RLS (tenant_isolation, FORCE) on tickets/projects/tasks scopes the LEFT JOINs to the caller's tenant under begin_with_tenant, so a stale or cross-tenant id resolves to a null name rather than leaking another tenant's work-item name.

Tests

time_entry_response_carries_work_item_names (HTTP-level, green against a throwaway Postgres):

  • a ticket-linked entry returns ticket_number + ticket_title = "Printer down", null project_name/task_title
  • a project+task entry returns project_name = "Delivery" + task_title = "Build", null ticket fields
  • asserted across both the get and list read paths

Full time_tracking suite: 6/6 (the existing happy-path, which lists with a ticket_id filter through the new joined query, still passes).

#PMS-332

## Summary The time-entries list could only render a bare `Ticket`/`Project`/`-` label because `TimeEntryResponse` carried ids but no work-item names. Task names in particular are not client-resolvable (there is no global tasks list endpoint, only `/projects/{id}/tasks` per project), so the rollup must come from the server. MAPPS-168 needs the real name, including the task title, on the list. ## Changes - Add four nullable fields to `TimeEntryResponse` and the internal `TimeEntryRow`: `ticket_number`, `ticket_title`, `project_name`, `task_title` (all `Option<String>`), populated in the `From<TimeEntryRow>` impl. - In both read paths (`list_time_entries`, `get_time_entry`), add `LEFT JOIN tickets / projects / tasks` and select `tk.ticket_number, tk.title AS ticket_title, pr.name AS project_name, ta.title AS task_title`. - Because the joined tables also carry `tenant_id`/`id`/`created_at`, the list query's data predicates and `ORDER BY` are qualified with the `te` alias to avoid ambiguous-column errors. The count query has no join and stays on bare columns. - Existing fields, filters, tenant scoping, and the single-query (no N+1) shape are unchanged. ## Tenant safety `time_entries.project_id` / `task_id` are bare UUIDs (no FK). RLS (`tenant_isolation`, FORCE) on `tickets`/`projects`/`tasks` scopes the LEFT JOINs to the caller's tenant under `begin_with_tenant`, so a stale or cross-tenant id resolves to a null name rather than leaking another tenant's work-item name. ## Tests `time_entry_response_carries_work_item_names` (HTTP-level, green against a throwaway Postgres): - a ticket-linked entry returns `ticket_number` + `ticket_title = "Printer down"`, null `project_name`/`task_title` - a project+task entry returns `project_name = "Delivery"` + `task_title = "Build"`, null ticket fields - asserted across both the get and list read paths Full `time_tracking` suite: 6/6 (the existing happy-path, which lists with a `ticket_id` filter through the new joined query, still passes). #PMS-332
feat(time): join work-item names into the time-entry response
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 38s
Check / fmt + clippy + compile + unit/doc tests (pull_request) Successful in 1m5s
Integration / integration tests (pull_request) Successful in 2m52s
Create release / Create release from merged PR (pull_request) Has been skipped
1e22e9c54b
The time-entries list could only render a bare "Ticket"/"Project"/"-" label because the response carried ids but no names, and task names are not client-resolvable (there is no global tasks list endpoint, only per-project). MAPPS-168 needs the real work-item name, including the task title, on the list.

Add four nullable fields to TimeEntryResponse and TimeEntryRow (ticket_number, ticket_title, project_name, task_title), populated by LEFT JOINs to tickets/projects/tasks in both the list and get read paths. Each is null when the entry has no link of that kind, or when the linked row is not visible (RLS on the joined tables scopes them to the caller's tenant, so a stale cross-tenant id resolves to null rather than leaking another tenant's name).

Because the joined tables also carry tenant_id/id/created_at, the list query's data predicates and ORDER BY are qualified with the te alias to avoid ambiguous-column errors; the count query has no join and stays on bare columns. Existing fields, filters, tenant scoping, and the single-query (no N+1) shape are unchanged.

A test asserts a ticket-linked entry returns ticket_number+title with null project/task, a project+task entry returns project_name+task_title with null ticket, across both the get and list paths.

#PMS-332

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/PMS-332-time-entry-work-item-names 2026-06-15 21:27:37 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!250
No description provided.