WorkestraDocs
PlatformTime Tracking

Custom Reports

Build any chart you want from your time data. Drag-drop widgets, eight chart types, save as a view, schedule email delivery.

The six built-in report tabs (Summary, Detailed, Weekly, Profitability, Workload, Utilization) cover the questions most teams ask. The Custom Report Builder at /time/reports/builder is for everything else — pivot member-billable hours by tag, donut your top clients by revenue, line-chart this quarter's billable trend per project. Anything you can group by + count.

Custom Report Builder

Screenshot needed — /time/reports/builder showing the filter row, the Add widget button, two configured charts side-by-side (a stacked bar and a donut), and a Save report input.

Where to find it

  • From /time/reports toolbar → Custom builder button
  • Direct URL: /time/reports/builder
  • A saved custom report on the Saved views popover routes back here automatically (saved views with widgets in their payload are recognised as custom reports)

How a widget is configured

Each widget answers four questions:

QuestionFieldExample
What chart?chartTypeBar, Stacked bar, Grouped bar, Donut, Line, Multi-line, Table, Pivot table
What goes on the primary axis?primaryGroupByProject, Member, Tag, Day, Week, Month, Billable
What goes on the secondary axis? (stacked / grouped / multi-line / pivot only)secondaryGroupBySame options as primary, must differ
What number to plot?metricHours, Billable hours, Revenue

Plus one knob: Top N rows (default 10) — caps the primary axis to the highest values so the chart stays readable on dense data.

Eight chart types at a glance

Chart typeBest forNeeds secondary?
Bar"Hours per project"No
Stacked bar"Hours per project, billable vs non-billable"Yes
Grouped bar"Hours per member, side-by-side per project"Yes
Donut"% of revenue by client"No
Line"Daily billable trend"No
Multi-line"Daily hours per project"Yes
TableSame data as Bar but sortable / denseNo
Pivot tableA full member × week matrixYes

Time-axis groupings (Day / Week / Month) are sorted ascending; categorical groupings sort by descending hours.

Building a report

  1. Open /time/reports/builder. You start with one default widget ("Hours by project"). It populates immediately so you see what works look like.
  2. Set the filter row at the top (date range, members, projects, tags). Filters apply to every widget on the report.
  3. Drag widgets to reorder — handles on the left edge of each card.
  4. Click Add widget to compose another. The dialog walks you through chart type → group-by → secondary (if needed) → metric → Top N.
  5. Hover any widget to edit (pencil) or delete (trash).
  6. Type a report name in the toolbar, click Save report.

Saved custom reports show up alongside your other saved views on the /time/reports toolbar's Saved views popover. They're tagged "scheduled" if you've also set up email delivery.

Saving and sharing

Custom reports persist as rows in the cross-cutting views table with a widgets field on the payload (forward-compatible — older saved views without widgets continue to render their named tab as before).

ActionWhere
Save a new custom reportToolbar Save report
Switch between your custom reportsToolbar dropdown labelled — My custom reports —
Open a custom report directly/time/reports/builder?viewId=<view-id>

Custom reports are per-user private by default. Mark a saved view as shared (uncheck Private when saving) to surface it for the whole workspace.

Scheduling email delivery

Any saved custom report can be emailed on a recurring schedule. The flow piggy-backs on the same scheduling backbone as the built-in tabs:

  1. Open the Saved views popover from the reports toolbar.
  2. Click the Clock icon on the saved custom report.
  3. Pick frequency (daily / weekly Monday / monthly 1st), enter recipient emails, Schedule.
  4. Manage all schedules at /time/reports/schedules — pause, resume, delete.

The cron at 06:20 UTC daily evaluates every enabled schedule, decides which ones fire today, and emails a digest with the report's KPIs + top-5 projects + top-10 entries plus a link to the live report. Per-recipient idempotency prevents duplicate sends if the cron retries.

See Reports → Scheduled email delivery for the full flow.

What it doesn't do (yet)

  • Pin to sidebar — Toggl-style "promote a saved report into the workspace nav" is on the roadmap. Today, custom reports are only reachable from the Saved views popover.
  • Cross-module reports — widgets only aggregate time_entries. Joining time + finance + projects in a single chart is out of scope; build cross-module dashboards on /dashboard instead.
  • Calculated fields — no formula columns yet (no "Hours × 1.2", no "this period vs. last period"). The metric is one of hours, billable_hours, billable_amount.

What's under the hood

Each widget loads through TimeCustomReportService.rollup(workspaceId, filters, primary, secondary?). The service runs one query against time_entries (excluding status='rejected'), filters server-side, and returns a normalised shape:

{ primary: [{key,label}],
  secondary: [{key,label}] | [],
  cells: { primaryKey: { secondaryKey: { hours, billable_hours, billable_amount } } },
  currency }

Every chart renderer (ChartRenderer in src/modules/time/components/reports/builder/chart-renderer.tsx) consumes this shape — flattening, pivoting, or rendering as a table according to the widget's chartType. No new dependency was introduced for the builder: Recharts powers the charts, @dnd-kit/sortable powers the reorder, the existing views table stores the config.

Tag-rolled-up widgets divide each entry's hours equally across that entry's tags, so totals don't double-count when an entry has multiple tags.


Next steps