feat(reports): saved custom-report definitions (PMS-457 phase 1) #340

Merged
YousifShkara merged 1 commit from feat/PMS-457-saved-reports into main 2026-06-23 11:12:30 +02:00
Owner

Adds tenant-scoped CRUD over saved_reports, the persistence layer the SPA's report-builder UI needs to save a user's "filtered, columned, grouped, sorted" definition between sessions. The execution runtime that compiles a row into a SELECT and streams results is Phase 2 of the ticket; phase 1 just persists the workflow.

Definition is JSONB on purpose:

  • filters mirrors the existing ticket / billing / time-entry list filters so the Phase 2 compiler can reuse those service methods rather than hand-rolling a query compiler;
  • columns is an ordered list of {field, header} so reordering an export is a no-op edit at the DB layer;
  • group_by / sort are small structured arrays.

entity_type is the discriminator the Phase 2 runtime uses to pick which service to compile against (tickets / time_entries / invoices / ...). VARCHAR(50) rather than an enum so a new entity (assets / projects) does not need a CHECK-widening migration; the runtime rejects unknown values at execution time.

Visibility model, pinned by tests/saved_reports.rs:

  • the author always sees their own reports (?scope=mine or default ?scope=any);
  • is_shared=true reports are visible to every user in the same tenant via the default scope and ?scope=shared;
  • only the AUTHOR may mutate or delete a report; a non-author hitting PATCH/DELETE on a shared row gets 404 (not 403), matching the absent-or-off-limits posture used elsewhere in the API;
  • ?entity_type= narrows the list so an entity-page sidebar can offer "saved reports targeting this entity" in one round trip.

Phase 2 (PMS-457 follow-up):

  • the actual compiler that takes a saved_reports row + a tenant and runs the underlying SELECT (paginated, streamed);
  • a /api/v1/reports/saved/{id}/execute endpoint that returns the result set;
  • export-to-CSV via the same compile path.

Phase 3 (scheduled_reports): cron + delivery target referencing a saved_reports.id. Out of scope here.

#PMS-457

Adds tenant-scoped CRUD over `saved_reports`, the persistence layer the SPA's report-builder UI needs to save a user's "filtered, columned, grouped, sorted" definition between sessions. The execution runtime that compiles a row into a SELECT and streams results is Phase 2 of the ticket; phase 1 just persists the workflow. Definition is JSONB on purpose: - `filters` mirrors the existing ticket / billing / time-entry list filters so the Phase 2 compiler can reuse those service methods rather than hand-rolling a query compiler; - `columns` is an ordered list of `{field, header}` so reordering an export is a no-op edit at the DB layer; - `group_by` / `sort` are small structured arrays. `entity_type` is the discriminator the Phase 2 runtime uses to pick which service to compile against (tickets / time_entries / invoices / ...). VARCHAR(50) rather than an enum so a new entity (assets / projects) does not need a CHECK-widening migration; the runtime rejects unknown values at execution time. Visibility model, pinned by `tests/saved_reports.rs`: - the author always sees their own reports (`?scope=mine` or default `?scope=any`); - `is_shared=true` reports are visible to every user in the same tenant via the default scope and `?scope=shared`; - only the AUTHOR may mutate or delete a report; a non-author hitting PATCH/DELETE on a shared row gets 404 (not 403), matching the absent-or-off-limits posture used elsewhere in the API; - `?entity_type=` narrows the list so an entity-page sidebar can offer "saved reports targeting this entity" in one round trip. Phase 2 (PMS-457 follow-up): - the actual compiler that takes a `saved_reports` row + a tenant and runs the underlying SELECT (paginated, streamed); - a `/api/v1/reports/saved/{id}/execute` endpoint that returns the result set; - export-to-CSV via the same compile path. Phase 3 (`scheduled_reports`): cron + delivery target referencing a `saved_reports.id`. Out of scope here. #PMS-457
feat(reports): saved custom-report definitions (PMS-457 phase 1)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m12s
E2E / Playwright against staging (pull_request) Successful in 57s
Create release / Create release from merged PR (pull_request) Successful in 1s
Integration / integration tests (pull_request) Successful in 4m59s
6706fa629a
Adds tenant-scoped CRUD over `saved_reports`, the persistence layer the SPA's report-builder UI needs to save a user's "filtered, columned, grouped, sorted" definition between sessions. The execution runtime that compiles a row into a SELECT and streams results is Phase 2 of the ticket; phase 1 just persists the workflow.

Definition is JSONB on purpose:
- `filters` mirrors the existing ticket / billing / time-entry list filters so the Phase 2 compiler can reuse those service methods rather than hand-rolling a query compiler;
- `columns` is an ordered list of `{field, header}` so reordering an export is a no-op edit at the DB layer;
- `group_by` / `sort` are small structured arrays.

`entity_type` is the discriminator the Phase 2 runtime uses to pick which service to compile against (tickets / time_entries / invoices / ...). VARCHAR(50) rather than an enum so a new entity (assets / projects) does not need a CHECK-widening migration; the runtime rejects unknown values at execution time.

Visibility model, pinned by `tests/saved_reports.rs`:
- the author always sees their own reports (`?scope=mine` or default `?scope=any`);
- `is_shared=true` reports are visible to every user in the same tenant via the default scope and `?scope=shared`;
- only the AUTHOR may mutate or delete a report; a non-author hitting PATCH/DELETE on a shared row gets 404 (not 403), matching the absent-or-off-limits posture used elsewhere in the API;
- `?entity_type=` narrows the list so an entity-page sidebar can offer "saved reports targeting this entity" in one round trip.

Phase 2 (PMS-457 follow-up):
- the actual compiler that takes a `saved_reports` row + a tenant and runs the underlying SELECT (paginated, streamed);
- a `/api/v1/reports/saved/{id}/execute` endpoint that returns the result set;
- export-to-CSV via the same compile path.

Phase 3 (`scheduled_reports`): cron + delivery target referencing a `saved_reports.id`. Out of scope here.

#PMS-457
YousifShkara deleted branch feat/PMS-457-saved-reports 2026-06-23 11:12:30 +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!340
No description provided.